Thread Synchronization
Often, multiple threads of execution manipulate a shared object in memory. When multiple threads share an object and that object is modified by one or more of the threads, indeterminate results may occur (as we will soon see in the chapter examples) unless the shared object is managed properly. If one thread is in the process of updating a shared object and another thread tries to update it too, it is possible that part of the object will reflect the information from one thread while another part of the object reflects information from a different thread. When this happens, the program's behavior cannot be trusted. Sometimes the program will produce the correct results, but other times it will produce incorrect results. In either case there will be no error message to indicate that the shared object was manipulated incorrectly.
The problem can be solved by giving one thread at a time exclusive access to code that manipulates the shared object. During that time, other threads desiring to manipulate the object are kept waiting. When the thread with exclusive access to the object finishes manipulating it, one of the threads that was kept waiting is allowed to proceed. In this fashion, each thread accessing the shared object excludes all other threads from doing so simultaneously. This is called mutual exclusion. Mutual exclusion allows the programmer to perform thread synchronization, which coordinates access to shared data by multiple concurrent threads.
Java uses locks to perform synchronization. Any object can contain an object that implements the Lock interface (package java.util.concurrent.locks). A thread calls the Lock's lock method to obtain the lock. Once a Lock has been obtained by one thread, the Lock object will not allow another thread to obtain the lock until the first thread releases the Lock (by calling the Lock's unlock method). If there are several threads trying to call method lock on the same Lock object at the same time, only one thread may obtain the lock at a timeall other threads attempting to obtain the Lock contained in the same object are placed in the waiting state for that lock. When a thread calls method unlock, the lock on the object is released and the highest-priority waiting thread attempting to lock the object proceeds. Class ReentrantLock (package java.util.concurrent.locks) is a basic implementation of the Lock interface. The constructor for a ReentrantLock takes a boolean argument that specifies whether the lock has a fairness policy. If this is set to true, the ReentrantLock's fairness policy states that the longest-waiting thread will acquire the lock when it is available. If this is set to false, there is no guarantee as to which waiting thread will acquire the lock when it is available.
Performance Tip 23.2
Using a Lock with a fairness policy helps avoid indefinite postponement, but can also dramatically reduce the overall efficiency of a program. Because of the large decrease in performance, fair locks are only necessary in extreme circumstances. |
If a thread that owns the lock on an object determines that it cannot continue with its task until some condition is satisfied, the thread can wait on a condition variable. This removes the thread from contention for the processor, places it in a wait queue for the condition variable and releases the lock on the object. Condition variables must be associated with a Lock and are created by calling Lock method newCondition, which returns an object that implements the Condition interface (package java.util.concurrent.locks). To wait on a condition variable, the thread can call the Condition's await method. This immediately releases the associated Lock and places the thread in the waiting state for that Condition. Other threads can then try to obtain the Lock. When a runnable thread completes a task and determines that the waiting thread can now continue, the runnable thread can call Condition method signal to allow a thread in that Condition's waiting state to return to the runnable state. At this point, the thread that transitioned from the waiting state to the runnable state can attempt to reacquire the Lock on the object. Even if it is able to reacquire the Lock, the thread still might not be able to perform its task at this timein which case the thread can call method await to release the Lock and reenter the waiting state. If multiple threads are in a Condition's waiting state when signal is called, the default implementation of Condition signals the longest-waiting thread to move to the runnable state. If a thread calls Condition method signalAll, then all the threads waiting for that condition move to the runnable state and become eligible to reacquire the Lock. Only one of those threads can obtain the Lock on the object at a timeother threads that attempt to acquire the same Lock will wait until the Lock becomes available again. If the Lock was created with a fairness policy, the longest-waiting thread will then acquire the Lock. When a thread is finished with a shared object, it must call method unlock to release the Lock.
Common Programming Error 23.1
Deadlock occurs when a waiting thread (let us call this thread1) cannot proceed because it is waiting (either directly or indirectly) for another thread (let us call this thread2) to proceed, while simultaneously thread2 cannot proceed because it is waiting (either directly or indirectly) for thread1 to proceed. Two threads are waiting for each other, so the actions that would enable each thread to continue execution never occur. |
Error-Prevention Tip 23.1
When multiple threads manipulate a shared object using locks, ensure that if one thread calls method await to enter the waiting state for a condition variable, a separate thread eventually will call Condition method signal to transition the thread waiting on the condition variable back to the runnable state. If multiple threads may be waiting on the condition variable, a separate thread can call Condition method signalAll as a safeguard to ensure that all the waiting threads have another opportunity to perform their tasks. If this is not done, indefinite postponement or deadlock could occur. |
Software Engineering Observation 23.1
The locking that occurs with the execution of the lock and unlock methods could lead to deadlock if the locks are never released. Calls to method unlock should be placed in finally blocks to ensure that locks are released and avoid these kinds of deadlocks. |
Performance Tip 23.3
Synchronization to achieve correctness in multithreaded programs can make programs run more slowly, as a result of thread overhead and the frequent transition of threads between the waiting and runnable states. There is not much to say, however, for highly efficient yet incorrect multithreaded programs! |
Common Programming Error 23.2
It is an error if a thread issues an await, a signal, or a signalAll on a condition variable without having acquired the lock for that condition variable. This causes an IllegalMonitorStateException. |