4.2 Condition Variables
Condition variables are a type
of synchronization provided by many other threading systems. A
condition variable is very similar to Java's
wait-and-notify mechanism—in fact, in most cases it is
functionally identical. The four basic functions of a POSIX condition
variable—wait(), timed_wait(), signal(), and broadcast(
)—map directly to the methods provided by Java
(wait(), wait(long),
notify(), and notifyAll(),
respectively). The implementations are also logically identical. The
wait() operation of a condition variable requires
that a mutex lock be held. It releases the lock while waiting and
reacquires the lock prior to returning to the caller. The
signal() function wakes up one thread whereas the
broadcast() function wakes up all the waiting
threads. These functions also require that the mutex be held during
the call. The race conditions of a condition variable are solved in
the same way as those of Java's wait-and-notify
mechanism.
There is one subtle difference, however. The wait-and-notify
mechanism is highly integrated with its associated lock. This makes
the mechanism easier to use than its condition variable counterpart.
Calling the wait() and notify() methods from synchronized sections of code is just a
natural part of their use. Using condition variables, however,
requires that you create a separate mutex lock, store that mutex, and
eventually destroy the mutex when it is no longer necessary.
Unfortunately, that convenience comes at a small price. A POSIX
condition variable and its associated mutex lock are separate
synchronization entities. It is possible to use the same mutex with
two different condition variables, or even to mix and match mutexes
and condition variables in any scope. While the wait-and-notify
mechanism is much easier to use and is usable for most cases of
signal-based synchronization, it is not capable of assigning any
synchronization lock to any notification object. When you need to
signal two different notification objects while requiring the same
synchronization lock to protect common data, a condition variable is
more efficient.
J2SE 5.0 adds a class that provides the functionality of condition
variables. This class is used in conjunction with the
Lock interface. Since this new interface (and,
therefore, object) is separate from the calling object and the lock
object, its usage is just as flexible as the condition variables in
other threading systems. In Java, condition variables are objects
that implement the Condition interface. The
Condition interface is tied to the
Lock interface, just as the wait-and-notify
mechanism is tied to the synchronization lock.
To create a Condition
object from the Lock object, you call a method
available on the Lock object:
Lock lockvar = new ReentrantLock( );
Condition condvar = lockvar.newCondition( );
Using the Condition object is similar to using the
wait-and-notify mechanism, with the Condition
object's await() and
signal() method calls replacing the
wait() and notify() methods.
We'll modify our typing program to use the condition
variable instead of the wait-and-notify methods. This time,
we'll show the implementation of the random
character generator; the code for the animation character class is
similar and can be found online.
package javathreads.examples.ch04.example3;
...
public class RandomCharacterGenerator extends Thread implements CharacterSource {
...
private Lock lock = new ReentrantLock( );
private Condition cv = lock.newCondition( );
...
public void run( ) {
try {
lock.lock( );
while (true) {
try {
if (done) {
cv.await( );
} else {
nextCharacter( );
cv.await(getPauseTime( ), TimeUnit.MILLISECONDS);
}
} catch (InterruptedException ie) {
return;
}
}
} finally {
lock.unlock( );
}
}
public void setDone(boolean b) {
try {
lock.lock( );
done = b;
if (!done) cv.signal( );
} finally {
lock.unlock( );
}
}
}
As we mentioned, a new Condition object is created
by calling the newCondition() method provided by
the Lock interface. This new
Condition object is bound to the
Lock instance whose method is called. This means
that the lock of the Lock instance must be held in
order to use the Condition object; it also means
that the Condition object releases and reacquires
the lock similar to the way Java's wait-and-notify
mechanism works with synchronization locks.
Therefore, our new random character generator now uses a
Lock object as its synchronization lock. We
instantiate a Condition object,
cv, which is set to the value returned by the
newCondition()
method of the lock object. Furthermore,
calls to the wait() and notify() method are replaced by the condition
object's await() and
signal() method.
In this example, it doesn't look like we
accomplished anything: all we do is use different methods to
accomplish what we were previously able to accomplish using the
wait-and-notify mechanism. In general, condition variables are
necessary for several reasons.
First, condition variables are needed when you use
Lock objects. Using the wait()
and notify() methods of the
Lock object will not work since these methods are
already used internally to implement the Lock
object. More importantly, just because you hold the
Lock object doesn't mean you hold
the synchronization lock of that object. In other words, the lock
represented by the Lock object and the
synchronization lock associated with the object are distinct. We need
a condition variable mechanism that understands the locking mechanism
provided by the Lock object. This condition
variable mechanism is provided by the Condition
object.
The second reason is the creation of the Condition
object. Unlike the Java wait-and-notify mechanism,
Condition objects are created as separate objects.
It is possible to create more than one Condition
object per lock object. That means we can target individual threads
or groups of threads independently. With the standard Java mechanism,
all waiting threads that are synchronizing on the same object are
also waiting on the same condition.
Here are all the methods of the
Condition
interface. These methods must be called while holding the lock of the
object to which the Condition object is tied:
- void await()
-
Waits for a condition to occur.
- void awaitUninterruptibly()
-
Waits for a condition to occur. Unlike the await() method, it is not possible to interrupt this call.
- long awaitNanos(long nanosTimeout)
-
Waits for a condition to occur. However, if the notification has not
occurred in nanosTimeout nanoseconds, it returns
anyway. The return value is an estimate of the timeout remaining; a
return value equal or less than zero indicates that the method is
returning due to the timeout. As usual, the actual resolution of this
method is platform-specific and usually takes milliseconds in
practice.
- boolean await(long time, TimeUnit unit)
-
Waits for a condition to occur. However, if the notification has not
occurred in the timeout specified by the time and
unit pair, it returns with a value of
false.
- boolean awaitUntil(Date deadline)
-
Waits for a condition to occur. However, if the notification has not
occurred by the absolute time specified, it returns with a value of
false.
- void signal()
-
Notifies a thread that is waiting using the
Condition object that the condition has occurred.
- void signalAll()
-
Notifies all the threads waiting using the
Condition object that the condition has occurred.
Basically, the methods of the Condition interface
duplicate the functionality of the wait-and-notify mechanism. A few
convenience methods allow the developer to avoid being interrupted or
to
specify
a timeout based on relative or absolute times.
|