Lock Conditions in Java
Introduction
Locking conditions provide the ability for a given thread to wait for some arbitrary condition to happen while executing a critical section of code. In this context, a critical section is a section of code that is usually protected by some kind of locking mechanism.
A thread may acquire an exclusive lock to a critical section of code and then notice that it does not hold the necessary conditions in order to proceed with the execution. This thread may then release the lock and change its state into a waiting state until the necessary conditions are met. This usually means that another thread will later signal the currently waiting thread, which in turn will re-acquire the lock and check if the necessary conditions for execution are already met, as we will see through this article.
This article complements other articles that cover locking mechanisms In Java, such as: Java Lock example and Read/Write Locks in Java.
Conditions
In order for a thread to suspend a critical code section execution, and consequently enter the waiting state, it must already hold a lock to that same critical code section. When a thread enters the waiting state it will atomically release the corresponding lock and suspend its execution. This means that as soon as a given thread suspends its execution and releases the lock, that same lock becomes available for grant by other threads. A simple example may look like the following:
class SharedResource { Object sharedResource; Lock lock = new ReentrantLock(); Condition condition = this.lock.newCondition(); public void writeSharedResource() { this.lock.lock(); try { while (!verifySomeCondition()) { try { this.condition.await(); } catch (InterruptedException e) { throw new RuntimeException("Thread interrupted"); } } // Write sharedResource sharedResource = ... ; } finally { this.lock.unlock(); } } }
We have omitted the verifySomeCondition() method, but you may assume that the method returns a value which checks if all conditions for thread execution are met (we will see a practical example through this article). If the conditions are not met, the thread will suspend its execution and release the lock in an atomic fashion. As we have said earlier, other threads may thereafter acquire the lock and step into the critical code section while the current thread is suspended (await()).
The thread will resume its execution as soon as another thread signals the condition:
// Signals a SINGLE thread that is currently waiting on the condition condition.signal(); // Signals ALL threads that are currently waiting on the condition condition.signalAll();
We should now mention a very important detail: Remember that the thread is suspended within a critical code section, ie. inside a lock/unlock code section. This means that, even if a given suspended thread receives a wake up signal, it will only resume its execution after no other thread is currently executing inside the critical code section. The critical code section between lock/unlock is always exclusive. Also keep in mind that when notifying a single thread via signal() method, we will wake up an arbitrary thread of the eventual waiting set of threads that are currently waiting on the condition, ie. we have no guarantees of which thread will be woken.
This mechanism is analogous with the intrinsic Java locking mechanism, ie. the java.lang.Object monitor wait(), notify() and notifyAll() methods. One of the main advantages of using the Condition interface in favor of the traditional monitor methods is the ability of using multiple wait condition sets:
Lock lock = new ReentrantLock(); Condition conditionA = this.lock.newCondition(); Condition conditionB = this.lock.newCondition();
This gives us the ability of waking up only a single set of waiting threads, as shown in the following typical Producer/Consumer example:
public class ProducerConsumer<T> { private T data; Lock lock = new ReentrantLock(); Condition dataCondition = this.lock.newCondition(); Condition noDataCondition = this.lock.newCondition(); public void put(T data) { if (data == null) { throw new IllegalArgumentException("data must not be null"); } this.lock.lock(); try { while (this.data != null) { try { this.noDataCondition.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } this.data = data; this.dataCondition.signal(); } finally { this.lock.unlock(); } } public T get() { this.lock.lock(); try { while (this.data == null) { try { this.dataCondition.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } T returnValue = this.data; this.data = null; this.noDataCondition.signal(); return returnValue; } finally { this.lock.unlock(); } } }
As you can see, we have defined two distinct wait condition sets, defined by the dataCondition and noDataCondition conditions respectively. This rather simple producer and consumer example accepts a single instance representing the data being produced and consumed. Each producer and consumer method - put() and get() - will test if there is enough space for publish data - in case of the publisher - or if there is any data ready to be consumed - in case of the consumer.
If the expected conditions are not met, the producer/consumer threads will suspend their execution using the respective condition: dataCondition or noDataCondition.
As soon as each thread is able to proceed, it will signal an eventual thread that may be waiting in the opposite side of the producer/consumer queue.
This model gives us the ability of signalling only a single thread of the opposite operation by the means of the signal() method, instead of having to signal all the waiting threads using the signalAll() method, thus improving our application performance. If there is a high number of waiting threads, it may be rather expensive - and absurd - to signal all of them when we already know that only a single thread will be able to proceed with the execution (the one which while() condition check will evaluate to true).
Lastly, we must mention another important detail: We should always wait inside a loop that tests the waiting condition. This is due to the spurious wake up phenomenon that is observed on suspended threads in some underlying platforms. Putting it simple, threads may be arbitrarily woken by the underlying platform without a signal taking place, so we must always wait in a loop that tests the waiting condition, otherwise we risk that suspended threads try to proceed without the waiting condition being met. More information on Spurious wakeup.
Condition waiting timeouts and interruption
Condition waiting methods provide some facilities in order to support wait timeouts and thread interruption.
condition.await(5L, TimeUnit.SECONDS);
We may wait for a specified amount of time by calling Condition#await(long time, TimeUnit unit). If the specified amount of time expires before an incoming signal, the thread will proceed its execution. This method is subject to interruptions invoked by other threads on the current waiting thread instance.
long seconds = 5L; long nanos = TimeUnit.SECONDS.toNanos(seconds); condition.awaitNanos(nanos);
Method Condition#await(long nanosTimeout) will wait for the specified amount of nanoseconds for an incoming signal. If the specified thread leaves the waiting state in order to respond to an incoming signal, it will return a long value which represents the remaining amount of time that is required to reach the initial specified timeout. This method is also subject to interruptions invoked by other threads on the current waiting thread instance.
Date date = ...; condition.awaitUntil(date);
Method Condition#awaitUntil(Date deadline) will wait for an incoming signal until the specified deadline is reached. This method returns a boolean which indicates if the specified deadline has already been reached. This method is also subject to interruptions invoked by other threads on the current waiting thread instance.
condition.awaitUninterruptibly();
Method Condition#awaitUninterruptibly() will wait for an incoming signal until the specified deadline is reached. This method returns a boolean which indicates if the specified deadline has already been reached. This method is not subject to interruptions invoked by other threads on the current waiting thread instance.