The Java language includes three important methods that effectively allow one thread to signal to another. Without this facility, various constructs used in concurrent programming would be difficult and inefficient to implement.
Put simply, this is how it works:
Before Java 5, the wait/notify mechanism was a key part of thread programming in Java. It was used in various situations such as the following:
As of Java 5, there is less need for programmers to use wait()/notify() directly, since other classes are available in the Java concurrency package (java.util.concurrent) to handle these common situations.
Imagine that we want to implement a connection pool: a list of Connection objects (encapsulating a connection to a database) of which we want to create a fixed number and share amongst various threads. As mentioned above, in Java 5 onwards, this wouldn't commonly be implemented by the application programmer using the wait/notify mechanism, since better-performing and higher level classes are available. But pre-Java 5, it was a common use for wait/notify.
The thread pooling problem is that we want to implement a call that allows any thread take a connection from the pool, if one is available, else wait for one to become available. And similarly, we want a call that allows the thread to return its connection to the pool. When a connection is returned to the pool, we want to 'hand' that connection to any waiting thread.
First, we create a ConnectionPool class and assume that it has a List of Connection objects. For the sake of argument, we'll just assume that all available connections are created when the ConnectionPool is constructed. (In reality, we'd probably want to create them on the fly, up to some maximum number.) We'll omit the part of createConnections method that actually creates the connections: the only interesting feature for our purposes is that we create a fixed number of connections and add them to an unsynchronized list: for reasons we'll see in a minute, we'll always synchronize explicitly on the list when accessing it.
public class ConnectionPool {
private List<Connection> connections = createConnections();
private List<Connection> createConnections() {
List<Connection> conns = new ArrayList<Connection>(5);
for (int i = 0; i < 5; i++) {
... add a Connection to conns
}
return conns;
}
}
Now we implement our getConnection() method. If no connection is currently available (i.e. connections is empty), then we need to wait until one becomes available. Then we return the first available connection.
public Connection getConnection() throws InterruptedException {
synchronized (connections) {
while (connections.isEmpty()) {
connections.wait();
}
return connections.remove(0);
}
}
Note first of all that we synchronize on the connection list. We then check if the list is empty. If, and while, it is, we "wait" on the list. In order to wait on an object, we must be synchronized on that object. But our thread will automatically release the lock temporarily while waiting. Calling wait() means that our thread will be suspended until it is "notified". Our thread will be "notified", and thus woken up, when another thread calls notify() on the object that we're waiting on (in this case, the connection list). When our thread wakes up, it automatically regains the lock. We can now check again that the list is not empty, and if it isn't, safely take out the first connection. This checking and removing will be atomic because we have the lock on the list. (If you're unsure what this means, see the section on the synchronized keyword in Java.)
Now, let's look at the other side of things: the method that a thread calls to return a connection to the pool:
public void returnConnection(Connection conn) {
synchronized (connections) {
connections.add(conn);
connections.notify();
}
}
Again with a synchronized lock on the list, we add the given connection to the list. Then, while still synchronized on it, we call notify() on the connection. Calling notify() means: "if there is at least one thread waiting on this object, please wake up one of those threads". In cases such as this, waking up a single random thread is the functionality we want: we've only added one connection to the list, so there's no point waking up more than one waiting thread. Note that we have no control over which waiting thread is woken up. In particular, we can't say "wake up the one that's been waiting longest". (Nor will most JVMs or OSs use such a policy: ensuring this kind of "fairness" turns out to decrease throughput considerably.)
Note that although notify() wakes up one of the waiting threads, the first thing that that thread needs to do is re-acquire the lock that our thread is currently holding. So after calling notify(), we should exit the synchronized block as quickly as possible. If we do something like this:
public void returnConnection(Connection conn) {
synchronized (connections) {
connections.add(conn);
connections.notify();
// bad: woken thread can't start until we
// come out of synchronized block!
updateStatistics(conn);
}
}
then the woken thread won't be able to proceed until our call to updateStatistics() returns.
A couple of small points about this wait-notify pattern are worth clarifying:
On the next page, we look at timed waits.
Written by Neil Coffey. Copyright © Javamex UK 2008. All rights reserved.