Synchronizing threads in Java, Part 1

Threads can do so much more than stream text along the bottom of your browser: synchronization is the key

The nice thing about threads in Java is that they are always there. This has hindered the porting of Java to some platforms not offering native threads (like Windows 3.1) because the person doing the port has to both port Java and create a threads package for it to use. Once you start to use threads effectively, you will not want to go back to more pedestrian single-threaded programming.

A Java programmer's first exposure to threads is usually an applet that uses them to provide animation. In these applets, the thread simply sleeps for a period of time before updating the next frame or moving text in an animated ticker. Threads, however, are much more useful than this. Another way to use threads is with the wait() and notify() functions that are part of the Object class.

Every Java object instance and class potentially has a monitor associated with it. I say potentially because if you don't use any of the synchronization functions, the monitor is never actually allocated, but it's waiting there just in case.

A monitor is simply a lock that serializes access to an object or a class. To gain access, a thread first acquires the necessary monitor, then proceeds. This happens automatically every time you enter a synchronized method. You create a synchronized method by specifying the keyword synchronized in the method's declaration.

During the execution of a synchronized method, the thread holds the monitor for that method's object, or if the method is static, it holds the monitor for that method's class. If another thread is executing the synchronized method, your thread is blocked until that thread releases the monitor (by either exiting the method or by calling wait()).

To explicitly gain access to an object's monitor, a thread calls a synchronized method within that object. To temporarily release the monitor, the thread calls the wait() function. Because the thread needs to have acquired the object's monitor, calling wait() is supported only inside a synchronized method. Using wait() in this way allows the thread to rendezvous with another thread at a particular synchronization point.

A very simple example of wait() and notify() is described in the following three classes.

The first class is named PingPong and consists of a single synchronized method and a state variable. The method is hit() and the only parameter it takes is the name of the player who will go next.

The algorithm is essentially this:

    If it is my turn,
        note whose turn it is next,
        then PING,
        and then notify anyone waiting.
    otherwise,
        wait to be notified.

To implement this, however, we add a few more lines:

 1    public class PingPong {
 2        // state variable identifying whose turn it is.
 3        private String whoseTurn = null;
 4
 5        public synchronized boolean hit(String opponent) {
 6          
 7      String x = Thread.currentThread().getName();
 8    
 9          if (whoseTurn == null) {
10          whoseTurn = x;
11              return true;
12      }
13    
14      if (x.compareTo(whoseTurn) == 0) {
15          System.out.println("PING! ("+x+")");
16          whoseTurn = opponent;
17          notifyAll();
18      } else {
19          try {
20              long t1 = System.currentTimeMillis();
21              wait(2500);
22              if ((System.currentTimeMillis() - t1) > 2500) {
23                  System.out.println("****** TIMEOUT! "+x+
24              " is waiting for "+whoseTurn+" to play.");
25              }
26          } catch (InterruptedException e) { }
27      }
28      return true; // keep playing.
29      }
30  }

In line 3 we declare our state variable, whoseTurn. This is declared private since the users of the class don't need to know it. Line 5 declares our method and it must have the synchronized keyword or the call to wait() will fail.

In line 7 we get our own name from the thread object. As you will see later, we set this after the thread is created. This helps in debugging since our thread is named something useful and is a convenient way to identify the players.

Lines 9 through 12 solve the problem of whose turn it is before anyone has gone. The policy implemented is that the first thread to invoke this method will get the honor of going first.

Lines 14 through 17 execute when it is the current thread's turn to go. When executed, the thread updates the state variable with the next thread's turn. This is done before the notify, as the notify may cause another thread to start running immediately before it knows it is its turn to run. Then notifyAll() is called to notify all threads that are waiting on this object that they can run. If you are using only two threads, simply call notify() since that call will wake up exactly one thread from the set waiting to run. With two threads, only one thread can be waiting, so the correct thread will wake up. If you extend this to three or more threads, however, the notify call may not wake up the correct thread and the system will stop until that thread's wait times out.

Lines 19 through 26 execute when it isn't the current thread's turn to go. Line 21 simply calls wait() and goes to sleep. However, you will notice that in line 20 the code notes the current time. It does this because when execution continues after the wait call returns, the reason for continuing could be either the wait timed out or our thread was awakened with a call to notify(). The only way to tell the difference is to measure how long the thread was asleep.

This timeout test is performed in line 22. If a timeout occurs, an informative message is printed to the console. In practice this will happen only when the time spent in lines 14 through 17 is greater than 2.5 seconds.

Line 26 is where we catch InterruptedException, which would be thrown if the thread in the wait() call stops prematurely.

Really, that is all there is to this part of the code. I did, however, add some additional code (shown below) between lines 8 and 9 to allow a third thread to cause the threads using this class to exit.

 8.01       if (whoseTurn.compareTo("DONE") == 0)
 8.02           return false;
 8.03
 8.04       if (opponent.compareTo("DONE") == 0) {
 8.05           whoseTurn = opponent;
 8.06           notifyAll();
 8.07           return false;
 8.08       }

As you can see, this is done by setting the special opponent DONE in the call to hit(). When the opponent is done, line 8.02 makes sure the code returns the boolean false.

Once we have the class of type PingPong, any thread with a reference to an instance of class PingPong can synchronize itself with other threads holding that same reference. To illustrate this, consider the following Player class designed for use in the instantiation of a couple of threads:

 1  public class Player implements Runnable {
 2      PingPong myTable;   // Table where they play
 3      String myOpponent;
 4
 5      public Player(String opponent, PingPong table) {
 6      myTable  = table;
 7      myOpponent = opponent;
 8      }
 9
10      public void run() {
11      while (myTable.hit(myOpponent))
12          ;
13      }
14  }

As you can see, this code is even simpler. All we really need is a class that implements the Runnable interface. The Thread class provides a constructor that takes a reference to an object implementing Runnable.

The two instance variables in this class are the reference holding the PingPong object and the name of this player's opponent. This latter field is used in the hit() method to tell the object which player should go next.

There is a single constructor taking a PingPong object and the name of an opponent. To satisfy the Runnable interface, there is the method run in lines 10 through 13.

The run method runs an infinite loop, calling hit() until it returns false. This method returns true until some thread calls it with the opponent name DONE.

To complete our example, we have an application class that will create a couple of threads using the Player class and pit them against each other. This is shown below in the Game class.

 1    public class Game {
 2
 3        public static void main(String args[]) {
 4            PingPong table = new PingPong();
 5        Thread alice = new Thread(new Player("bob", table));
 6        Thread bob   = new Thread(new Player("alice", table));
 7
 8        alice.setName("alice");
 9        bob.setName("bob");
10        alice.start();    // alice starts playing
11        bob.start();  // bob starts playing
12        try {
13            // Wait 5 seconds
14            Thread.currentThread().sleep(5000);
15        } catch (InterruptedException e) { }
16
17        table.hit("DONE"); // cause the players to quit their threads.
18        try {
19            Thread.currentThread().sleep(100);
20        } catch (InterruptedException e) { }
21        }
22    }

Because we want to execute this class from the command line, it must include a public static method named main that takes a single argument that is an array of strings. This is the method signature the java command keys off of when instantiating a class from the command line.

Line 4 is where the code instantiates a copy of our PingPong class and stores the reference in the local variable table. Line 5 and line 6 are compound object creations, first creating new Player objects and then using those objects in the creation of new Thread objects. At create time, the name of the opponent is specified so Alice's opponent is Bob and Bob's opponent is Alice. These new threads are named using the setName method in lines 8 and 9, and then they are started in lines 10 and 11.

After line 11 is executed, there are three user threads running, one named alice, one named bob, and the main thread. On the system console you will start seeing messages of the form:

PING! (alice)
PING! (bob)
PING! (alice)
...

and so on. The threads alternate which one runs by the state in the PingPong object. This object forces them to run one after another, however it also ensures that they run as rapidly after one another as possible since as soon as one is finished, it calls notifyAll() and the other thread begins to run.

Finally, in lines 12 through 15 you will see that the main thread goes to sleep for five seconds or so, and when it wakes up, it calls hit() with the magic bullet name DONE. This will cause the alice and bob threads to exit. Due to a bug in the Windows version of the Java runtime, the main thread has to wait a bit to let alice and bob exit first, before it can exit. Otherwise it will never exit (Sun knows about this bug). The short sleep in lines 18 through 20 cover this case and allow our program to exit normally on all systems.

So we have managed to get two threads to share the processor equally by synchronizing use of a common object instance. If you have followed the discussion and believe you understand the code, test your understanding by adding another thread to the mix and call it Chuck. After you've done that, answer these questions for yourself:

  • Who is Chuck's opponent?
  • What happens if two threads have the same opponent?
  • What happens if a thread is nobody's opponent?
Chuck McManis is currently the Director of Technology at GolfWeb Inc., a Web magazine devoted to the game of golf. His role there is to develop technologies that make the presentation of the magazine interactive, compelling, and enjoyable. Before joining GolfWeb he was a member of the Java group. He joined the Java group just after the formation of FirstPerson Inc. and was a member of the portable OS group (the group responsible for the OS portion of Java). Later, when FirstPerson was dissolved, he stayed with the group through the development of the alpha and beta versions of the software. He was responsible for creating the Java version of the Sun home page in May 1995. He also developed a cryptographic library for Java and versions of the Java class loader that could screen classes based on Digital Signatures. Before joining FirstPerson, Chuck worked in the Operating Systems area of SunSoft developing networking applications, where he did the initial design of NIS+.
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more