Synchronization

Synchronization and happens-before

We saw last time that a write to memory by one thread (i.e., an assignment to an instance variable) does not necessarily affect a later read from the same location by another thread. This happens because Java, like most programming languages, offers only a weak consistency model in which only some writes are guaranteed to be seen by later reads. When the consistency model considers two operations to be causally related in this way, we say that one operation has a happens-before relationship to the other. A write that happens-before a read is guaranteed to be seen by the read (though it is possible that the read will return the result of an even later write). Conversely, a read that happens-before a write is guarantee not to see the written value.

Release consistency

Different weak consistency models make different guarantees about happens-before relationships. However, a useful least common denominator is release consistency. It guarantees that any operation to a location that occurs before a release of a mutex by one thread will happen-before any operation following a later acquire of that mutex. Thus, to make sure that updates done to shared state by one thread are seen by other threads, we simply have to guard all accesses to that shared state using a mutex.

Barriers

In scientific computing applications, barriers are another popular way to ensure that updates by one thread or set of threads are seen by computation in other threads. A barrier is created with a specified number of threads that must reach the barrier. Each thread that reaches the barrier will block until the specified number of threads have all reached it, at which point all the threads unblock and are able to go forward. All operations in all threads that occur before the barrier is reached are guaranteed to happen-before all operations that occur after the barrier. Barriers make it easy to divide up a parallel computation into a series of communicating stages.

The Java libraries provide a barrier abstraction, the class java.util.concurrent.CyclicBarrier. An instance of this class is created with the number of threads that are expected to reach the barrier. Each thread reaches the barrier by calling barrier.await(). This cause the threads to block until the required number of threads reaches the barrier, at which point all the threads unblock. The barrier then resets and can be used again.

Barriers also help ensure a consistent view of memory. Once a thread that has reached a barrier unblocks, it is guaranteed to see all the memory updates that other threads performed before the barrier. The barrier style of computation allows a set of threads to divide up work and make progress on it, then exchange information via a barrier.

Monitors

The monitor pattern is another way to manage synchronization. It builds synchronization into objects: a monitor is an object with a built-in mutex on which all of the monitor's methods are synchronized. This design is accomplished in Java easily, because every object has a mutex, and the synchronized keyword enforces the monitor pattern. Java objects are designed to be used as monitors. A monitor can also have some number of condition variables, which we'll return to shortly.

The only objects that should be shared between threads are therefore immutable objects and objects protected by locks. Objects protected by locks include both monitors and objects encapsulated inside monitors, since objects encapsulated inside monitors are protected by their locks.

Deadlock

Monitors ensure consistency of data. But the locking they engage in can cause deadlock, a condition in which every thread is waiting to acquire a lock that some other thread is holding. For example, consider two monitors a and b, where a.f() calls b.g() and vice-versa. If two threads try to call a.f() and b.g() respectively, the threads will acquire locks on a and b respectively and then deadlock trying to acquire the other lock. We can represent this situation using the diagram in the figure. In such a diagram, deadlocks show up as cycles in the graph.

To avoid creating cycles in the graph, the usual approach is to define an ordering on locks, and acquire locks only consistent with that ordering. For example, we might decide that a < b in the lock ordering. Therefore b cannot call a method of a because a method of b already holds a lock that is higher in the ordering.

The requirement that some locks not be held becomes a precondition of methods, which need to specify which locks may be held when the method is called. To abstract this sometimes a notion of locking level is defined. The locking level defines the highest level lock in the lock ordering that may be held when the method is called. For example

Locks are not enough for waiting

Locks block threads from making progress, but they are not sufficient for blocking threads in general. In general we may want to block a thread until some condition becomes true. Examples of such situations are (1) when we want to communicate information between threads (which may need to block until some information becomes available) and (2) when we want to implement our own lock abstractions.

One such abstraction we might want to build is a barrier, because for simple uses of concurrency, barriers make it easy to build race-free, deadlock-free code. But thus far, we haven't seen a

For example, suppose we want to run two threads in parallel to compute some results and wait until both results are available. We might define a class WorkerPair that spawns two worker threads:

class WorkerPair extends Runnable {
    int done; // number of threads that have finished
    Object result;
    WorkerPair() {
	done = 0;
	new Thread(this).start();
	new Thread(this).start();
    }
    public void run() {
	doWork();
	synchronized(this) {
	    done++;
	    result = ...
	}
    }

    // not synchronized, to allow concurrent execution<
    public void doWork() {
	// use synchronized methods here
    }

    Object getResult() {
	while (done < 2) {} // oops: wasteful!
	return result; // oops: not synchronized!
    }
}

We might then use this code as follows:

w = new WorkerPair();
Object o = w.getResult();

As the comments in the code suggest, there are two serious problems with the getResult implementation. First, the loop on done < 2 will waste a lot of time and energy. Second, there is no synchronization ensuring that updates to result are seen.

How can we fix this? We might start by making getResult() synchronized, but this would block the final assignment to done and result in the run method. We can't use the mutex of w to wait until done becomes 2.

Condition variables

The solution to the problem is to use a condition variable, which is a mechanism for blocking a thread until a condition becomes true.

While monitors in general have multiple condition variables, every Java object implicitly has a single condition variable tied to its mutex. It is accessed using the wait() and notifyAll() methods. (There is also a notify() method, but it should usually be avoided.)

The wait() method is used when the thread wants to wait for the condition to become true. It may only be called when the mutex is held. It atomically releases the mutex and blocks the current thread on the condition variable. The thread will only wake up and start executing when notifyAll() or notify() are called on the same condition variable. (Java has a version of wait() that includes a timeout period after which it will automatically wake itself up. This version should usually be avoided.) In particular, wait() will not wake up simply because the condition variable's mutex has been released by some other thread. The other thread must call notifyAll().

Another thread should call the notifyAll() method when the condition of the condition variable might be true. Its effect is to wake up all threads waiting on the condition variable. When a thread wakes up from wait(), it immediately tries acquire the mutex. Only one thread can win; the others all block waiting for the winner to release the mutex. Eventually they acquire the mutex, though there is no guarantee that the condition is true when any of the threads awakes.

After a thread calls wait(), the condition it is waiting for might be true when wait() returns. But it need not be. Some other thread might have been scheduled first and may have made the condition false. So wait() is always called in a loop, like so:

while (!condition) wait();

Failure to test the condition after wait() leads to what is called a wakeup--waiting race, in which threads awakened by notifyAll() race to observe the condition as true. The winners of the race can then spoil things for later awakeners.

Using condition variables, we can implement getResult() as follows:

synchronized Object getResult() {
    while (done < 2) wait();
    return result;
}

With this implementation, the mutex is not held while the thread waits. The implementation of run is also modified to call notifyAll():

...
synchronized(this) {
    done++;
    result = ...
    if (done == 2) notifyAll();
}

In Java, the call to notifyAll() must be done when the mutex is held. Waiting threads will awaken but will immediately block trying to acquire the mutex. If there are threads waiting, one of them will win the race and acquire the mutex. Since each woken thread will test the condition, we need not even test it before calling notifyAll():

...
synchronized(this) {
    done++;
    result = ...
    notifyAll();
}

Java objects also have a notify() method that wakes just one thread instead of all of them. Using notify() is error-prone and usually should be avoided.

In general a monitor may have multiple conditions under which it wants to wake up threads. Given that a Java object has only one built-in condition variable, how can this be managed? One possibility is to use a ConditionObject object from the java.util.concurrent package. A second easy technique is to combine all the multiple conditions into one condition variable that represents the boolean disjunction of all of them. A notifyAll() is sent whenever any of the conditions becomes true; threads awoken by notifyAll() then test to see if their particular condition has become true; otherwise, they go back to sleep.

Using background threads with JavaFX

In JavaFX, any background work must be done in a separate thread, because if the Application thread is busy doing work instead of handling user interface events, the UI becomes unresponsive. However, UI nodes are not thread-safe, so only one thread is allowed to access the component hierarchy: the Application thread.

The Task class encapsulates useful functionality for starting up background threads and for obtaining results from them. This is easier than coding up your own mechanism using mutexes and condition variables. The key methods are these:

Task.java

Some of the methods are designed to be used within the implementation of the task, and others are designed to be used by client code in other threads, to control the task and to interact with it.

To compute something of type V in the background, a subclass of Task<V> is defined that overrides the method call(). Because a Task is a Runnable, the task can be started by creating a new thread to run it:

Thread th = new Thread(task);
th.start();

The work done by the tasks is defined in the call() method; it should simply return the desired result at the end of the method in the usual way. Notice that the call() method is not supposed to be called by clients or by any subclass code; instead, it is automatically called by the run method of the task.

To report progress back to the Application thread, it may also call reportProgress(). When the task completes by returning a value of type V from the call() method, the event handler h defined by calling setOnSucceeded(h) is invoked in the Application thread.

It is possible for a task to be canceled by calling the cancel() method; however, it is incumbent on the implementation of the task to periodically check whether the task has been canceled by using the isCancelled() method.

By listening on the property progressProperty(), client code in the Application thread can keep track of the progress of the task and update the GUI to reflect how far along the task is. The Task can also communicate back to the Application thread by using method Platform.runLater(), but this approach may couple the task implementation with the GUI more than is desirable.