Summary
Java's built-in multithreading capability makes it easy to build powerful multiprocessing applets. Synchronizing the activity of these separate executing entities is crucial to reliable and predictable applet behavior. Walk through a simple example illustrating the use of thewait()
andnotify()
functions. (2,000 words)
he 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:
About the author
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+.