Java's answer to the traditional mutex is the reentrant lock, which comes with additional bells and whistles. It is similar to the implicit monitor lock accessed when using synchronized methods or blocks. With the reentrant lock, you are free to lock and unlock it in different methods but not with different threads. If you attempt to unlock a reentrant lock object by a thread which didn't lock it initially, you'll get an IllegalMonitorStateException. This behavior is similar to when a thread attempts to unlock a pthread mutex.
When locks get acquired by threads, there's no guarantee of the order in which threads are granted access to a lock. A thread requesting lock access more frequently may be able to acquire the lock unfairly greater number of times than other locks. Java locks can be turned into fair locks by passing in the fair constructor parameter. However, fair locks exhibit lower throughput and are slower compared to their unfair counterparts.
We saw how each java object exposes the three methods, wait()
,notify()
and notifyAll()
which can be used to suspend threads till some condition becomes true. You can think of Condition as factoring out these three methods of the object monitor into separate objects so that there can be multiple wait-sets per object. As a reentrant lock replaces synchronized
blocks or methods, a condition replaces the object monitor methods. In the same vein, one can't invoke the condition variable's methods without acquiring the associated lock, just like one can't wait on an object's monitor without synchronizing on the object first. In fact, a reentrant lock exposes an API to create new condition variables, like so:
Lock lock = new ReentrantLock();
Condition myCondition = lock.newCondition();
Notice, how can we now have multiple condition variables associated with the same lock. In the synchronized
paradigm, we could only have one wait-set associated with each object.
Spurious mean fake or false. A spurious wakeup means a thread is woken up even though no signal has been received. Spurious wakeups are a reality and are one of the reasons why the pattern for waiting on a condition variable happens in a while loop as discussed in earlier chapters. There are technical reasons beyond our current scope as to why spurious wakeups happen, but for the curious on POSIX based operating systems when a process is signaled, all its waiting threads are woken up. Below comment is a directly lifted from Java's documentation for the wait(long timeout) method.
* A thread can also wake up without being notified, interrupted, or
* timing out, a so-called <i>spurious wakeup</i>. While this will rarely
* occur in practice, applications must guard against it by testing for
* the condition that should have caused the thread to be awakened and
* continuing to wait if the condition is not satisfied. In other words,
* waits should always occur in loops, like this one:
*
* synchronized (obj) {
* while (condition does not hold)
* obj.wait(timeout);
* ... // Perform action appropriate to condition
* }
*
A missed signal happens when a signal is sent by a thread before the other thread starts waiting on a condition. This is exemplified by the following code snippet. Missed signals are caused by using the wrong concurrency constructs. In the example below, a condition variable is used to coordinate between the signaller and the waiter thread. The condition is signaled at a time when no thread is waiting on it causing a missed signal.
In later sections, you'll learn that the way we are using the condition variable's await
method is incorrect. The idiomatic way of using await
is in a while loop with an associated boolean condition. For now, observe the possibility of losing signals between threads.
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class Demonstration {
public static void main(String args[]) throws InterruptedException {
MissedSignalExample.example();
}
}
class MissedSignalExample {
public static void example() throws InterruptedException {
final ReentrantLock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
Thread signaller = new Thread(new Runnable() {
public void run() {
lock.lock();
condition.signal();
System.out.println("Sent signal");
lock.unlock();
}
});
Thread waiter = new Thread(new Runnable() {
public void run() {
lock.lock();
try {
condition.await();
System.out.println("Received signal");
} catch (InterruptedException ie) {
// handle interruption
}
lock.unlock();
}
});
signaller.start();
signaller.join();
waiter.start();
waiter.join();
System.out.println("Program Exiting.");
}
}
The above code when ran, will never print the statement Program Exiting and execution would time out. Apart from refactoring the code to match the idiomatic usage of condition variables in a while loop, the other possible fix is to use a semaphore for signalling between the two threads as shown below
import java.util.concurrent.Semaphore;
class Demonstration {
public static void main(String args[]) throws InterruptedException {
FixedMissedSignalExample.example();
}
}
class FixedMissedSignalExample {
public static void example() throws InterruptedException {
final Semaphore semaphore = new Semaphore(1);
Thread signaller = new Thread(new Runnable() {
public void run() {
semaphore.release();
System.out.println("Sent signal");
}
});
Thread waiter = new Thread(new Runnable() {
public void run() {
try {
semaphore.acquire();
System.out.println("Received signal");
} catch (InterruptedException ie) {
// handle interruption
}
}
});
signaller.start();
signaller.join();
Thread.sleep(5000);
waiter.start();
waiter.join();
System.out.println("Program Exiting.");
}
}
Java's semaphore can be releas()-ed or acquire()-d for signalling amongst threads. However the important call out when using semaphores is to make sure that the permits acquired should equal permits returned. Take a look at the following example, where a runtime exception causes a deadlock.