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.