Lecture 19: Synchronization

Mutexes and synchronized

Mutexes are mutual exclusion locks. There are two main operations on mutexes: acquire() and release(). The acquire() operation tries to acquire the mutex for the current thread. At most one thread can hold a mutex at a time. While a lock is being held by a thread, all other threads that try to acquire the lock will be blocked until the lock is released, at which point just one waiting thread will manage to acquire it.

Java supports mutexes directly. Every object has a mutex implicitly associated with it. There is no way to directly invoke the acquire() and release() operations on an object o; instead, we use the synchronized statement to acquire the object's mutex, to perform some action, and to release the mutex:

synchronized (o) {
    // ...perform some action while holding o's mutex...
}

The synchronized statement is useful because it makes sure that the mutex is released no matter how the statement finishing executing, even it is through an exception. You can't call the underlying acquire() and release() operations explicitly, but if you could, the above code using synchronized would be equivalent to this:

try {
    o.acquire();
    // ...perform some action while holding o's mutex...
} finally {
    o.release();
}

Mutexes take up space, but a mutex is created for an object only when the object is first used for a synchronized statement, so normally they don't add much overhead.

Using mutexes we can protect the withdraw() and deposit() methods from themselves and from each other, using the receiver object's mutex:

void withdraw(int n) {
  synchronized(this) {
    balance -= n;
  }
}

void deposit(int n) {
  synchronized(this) {
    balance += n;
  }
}

Because the pattern of wrapping entire method bodies in synchronized(this) is so common, Java has syntactic sugar for it. Declaring a method to be synchronized has the same effect:

synchronized void withdraw(int n) {
    balance -= n;
}
synchronized void deposit(int n) {
    balance += n;
}

When is synchronization needed?

Synchronization is not free. It involves manipulating data structures, and on a machine with multiple processors (or cores), requires communication between the processors. When one is trying to make code run fast, it is tempting to cheat on synchronization. Usually this leads to disaster, as we will see when we think about when synchronization is needed.

Synchronization is needed whenever we need to rely on invariants on the state of objects, either between different fields of one or more objects, or between contents of the same field at different times. Without synchronization there is no guarantee that some other thread won't be simultaneously modifying the fields in question, leading to an inconsistent view of their contents.

Synchronization is also needed when we need to make sure that one thread sees the updates caused by another thread. This is because different threads may be running on different processors. For speed, each processor has its own local copy of memory. Updates to local memory may not propagate to other processors. For example, consider two threads executing the following code in parallel:

Thread 1:
y = 1;
x = 1;
Thread 2:
while (x != 0) {}
print (y);

What possible values of y might be printed by thread 2? Naively it looks like the only possible value is 1. But without synchronization between these two threads, the update to x can be seen by thread 2 without seeing the update to y. The fact that the assignment to y happened before x does not matter. The only reliable notion of happens-before between two different threads is that enforced by explicit thread synchronization.

Therefore synchronization is needed for all accesses to mutable state that is shared between threads. The mutable state might be entire objects, or, for finer-grained synchronization, just mutable fields of objects. When the lock protecting a shared mutable field is not being held by the current thread, the programmer should assume that its value can change at any time. Any invariant that involves the value of such a field cannot be relied upon.

Note that immutable state that is shared between threads doesn't need to be locked because no one will trying to update them. This fact encourages a style of programming that avoids mutable state.

The monitor pattern

The monitor pattern 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 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.

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.

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;
    Worker() {
	done = 0;
	new Thread(this).start();
	new Thread(this).start();
    }
    public void run() {
	// note: not synchronized, to allow concurrent execution
	...
	// do some work using synchronized methods
	...
	synchronized(this) {
	    done++;
	    result = ...
	}
    }
    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.

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();
}

The call to notifyAll() can be done when the mutex is held but need not be. And since the 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.

Using background threads with Swing

Swing is a single-threaded toolkit, so only one thread should try to manipulate the component hierarchy: the event dispatch thread. Any background work must be done in a separate thread, because if the event dispatch thread is busy doing work, the UI becomes unresponsive.

The SwingWorker class encapsulates the key 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.

class SwingWorker {
    /** Schedule this worker to be run on a background thread.
      * Requires: must be called by event dispatch thread. */
   public void execute();

   /** Do some work in the worker thread.
     * Overridable: do nothing. */
   public void doInBackground();

   /** Invoked in the the event dispatch thread when doInBackground
     * finishes.
     * Overridable: do nothing. */
   public void done();

   ...
}

To start a background thread, an instance of a subclass of SwingWorker is created in a listener and its execute() method is called. The work it does is defined by its doInBackground method, which is overridden by the subclass. The event dispatch thread is notified of the completion of the work by a call to done(), which is overridden by the subclass.

If intermediate results need to be passed back to the event dispatch thread during execution of the background thread, there are two more methods for this purpose, called publish() and process(). The first is called in the worker thread to make results available; this causes the second method, process() to be called in the event dispatch thread, passing it whatever results were published.