Every thread has a life of its own. Normally, a thread goes about its business without any regard for what other threads in the application are doing. Threads may be time-sliced, which means they can run in arbitrary spurts and bursts as directed by the operating system. On a multiprocessor system, it is even possible for many different threads to be running simultaneously on different CPUs. This section is about coordinating the activities of two or more threads, so they can work together and not collide in their use of the same address space.
Java provides a few simple structures for synchronizing the activities of threads. They are all based on the concept of monitors, a widely used synchronization scheme developed by C.A.R. Hoare. You don’t have to know the details about how monitors work to be able to use them, but it may help you to have a picture in mind.
A monitor is essentially a lock. The lock is attached to a resource that many threads may need to access, but that should be accessed by only one thread at a time. It’s not unlike a restroom with a door that locks. If the resource is not being used, the thread can acquire the lock and access the resource. By the same token, if the restroom is unlocked, you can enter and lock the door. When the thread is done, it relinquishes the lock, just as you unlock the door and leave it open for the next person. However, if another thread already has the lock for the resource, all other threads have to wait until the current thread finishes and releases the lock. This is just like when the restroom is locked when you arrive: you have to wait until the current occupant is done and unlocks the door.
Fortunately, Java makes the process of synchronizing access to resources quite easy. The language handles setting up and acquiring locks; all you have to do is specify which resources require locks.
The most common need for
synchronization among threads in Java is to serialize their access to
some resource (an object)—in other words, to make sure that
only one thread at a time can manipulate an object or
variable.[26] In Java, every object has a lock associated
with it. To be more specific, every class and every instance of a
class has its own lock. The synchronized
keyword
marks places where a thread must acquire the lock before proceeding.
For example, say we
implemented a SpeechSynthesizer
class that
contains a say( )
method. We don’t want
multiple threads calling say( )
at the same time
or we wouldn’t be able to understand anything being said. So we
mark the say( )
method as
synchronized
, which means that a thread has to
acquire the lock on the SpeechSynthesizer
object
before it can speak:
class SpeechSynthesizer { synchronized void say( String words ) { // speak } }
Because say( )
is an instance method, a thread has
to acquire the lock on the particular
SpeechSynthesizer
instance it is using before it
can invoke the say( )
method. When say( )
has completed, it gives up the lock, which allows the
next waiting thread to acquire the lock and run the method. Note that
it doesn’t matter whether the thread is owned by the
SpeechSynthesizer
itself or some other object;
every thread has to acquire the same lock, that of the
SpeechSynthesizer
instance. If say( )
were a class (static) method instead of an instance
method, we could still mark it as synchronized. But in this case as
there is no instance object involved, the lock would be on the class
object itself.
Often, you want to synchronize multiple methods of the same class, so
that only one of the methods modifies or examines parts of the class
at a time. All static synchronized methods in a class use the same
class object lock. By the same token, all instance methods in a class
use the same instance object lock. In this way, Java can guarantee
that only one of a set of synchronized methods is running at a time.
For example, a SpreadSheet
class might contain a
number of instance variables that represent cell values, as well as
some methods that manipulate the cells in a row:
class SpreadSheet { int cellA1, cellA2, cellA3; synchronized int sumRow( ) { return cellA1 + cellA2 + cellA3; } synchronized void setRow( int a1, int a2, int a3 ) { cellA1 = a1; cellA2 = a2; cellA3 = a3; } ... }
In this example, both methods setRow()
and
sumRow( )
access the cell values. You can see that
problems might arise if one thread were changing the values of the
variables in setRow( )
at the same moment another
thread were reading the values in sumRow( )
. To
prevent this, we have marked both methods as
synchronized
. When threads are synchronized, only
one will be run at a time. If a thread is in the middle of executing
setRow()
when another thread calls
sumRow( )
, the second thread waits until the first
one is done executing setRow( )
before it gets to
run sumRow( )
. This synchronization allows us to
preserve the consistency of the SpreadSheet
. And
the best part is that all of this locking and waiting is handled by
Java; it’s transparent to the programmer.
In
addition to synchronizing entire methods, the
synchronized
keyword can be used in a special
construct to guard arbitrary blocks of code. In this form it also
takes an explicit argument that specifies the object for which it is
to acquire a lock:
synchronized ( myObject ) { // Functionality that needs to be synced }
This code block can appear in any method. When it is reached, the
thread has to acquire the lock on myObject
before
proceeding. In this way, we can synchronize methods (or parts of
methods) in different classes in the same way as methods in the same
class.
A synchronized instance method is, therefore, equivalent to a method with its statements synchronized on the current object. Thus:
synchronized void myMethod ( ) { ... }
is equivalent to:
void myMethod ( ) { synchronized ( this ) { ... } }
In
the SpreadSheet
example, we guarded access to a
set of instance variables with a synchronized method, which we did
mainly so that we wouldn’t change one of the variables while
someone was reading the rest of them. We wanted to keep them
coordinated. But what about individual variable types? Do they need
to be synchronized? Normally the answer is no. Almost all operations
on primitives and object reference types in Java happen
“atomically”: they are handled by the virtual machine in
one step, with no opportunity for two threads to collide. You
can’t be in the middle of changing a reference and be only
“part way” done when another thread looks at the
reference.
But watch out—we did say “almost.” If you read the
Java virtual machine specification carefully, you will see that the
double and long primitive types are not guaranteed to be handled
atomically. Both of these types represent 64-bit values. The problem
has to do with how the Java Virtual Machine’s stack handles
them. It is possible that this specification will be beefed up in the
future. But for now, if you have any fears, synchronize access to
your double
and long
instance
variables through accessor methods.
With the
synchronized
keyword, we can serialize the
execution of complete methods and blocks of code. The
wait()
and notify( )
methods of
the Object
class extend this capability. Every
object in Java is a subclass of Object
, so every
object inherits these methods. By using wait()
and
notify( )
, a thread can effectively give up its
hold on a lock at an arbitrary point and then wait for another thread
to give it back before continuing.[27] All of the coordinated activity still
happens inside of synchronized blocks, and still only one thread is
executing at a given time.
By executing wait( )
from a synchronized block, a
thread gives up its hold on the lock and goes to sleep. A thread
might do this if it needs to wait for something to happen in another
part of the application, as we’ll see shortly. Later, when the
necessary event happens, the thread that is running it calls
notify( )
from a block synchronized on the same
object. Now the first thread wakes up and begins trying to acquire
the lock again.
When the first thread manages to reacquire the lock, it continues
from the point it left off. However, the thread that waited may not
get the lock immediately (or perhaps ever). It depends on when the
second thread eventually releases the lock and which thread manages
to snag it next. Note also that the first thread won’t wake up
from the wait()
unless another thread calls
notify( )
. There is an overloaded version of
wait( )
, however, that allows us to specify a
timeout period. If another thread doesn’t call notify( )
in the specified period, the waiting thread automatically
wakes up.
Let’s look at a simple scenario to see what’s going on.
In the following example, we’ll assume there are three
threads—one waiting to execute each of the three synchronized
methods of the MyThing
class. We’ll call
them the waiter, notifier,
and related threads. Here’s a code
fragment to illustrate:
class MyThing { synchronized void waiterMethod( ) { // do some stuff wait( ); // now wait for notifier to do something // continue where we left off } synchronized void notifierMethod( ) { // do some stuff notify( ); // notify waiter that we've done it // do more things } synchronized void relatedMethod( ) { // do some related stuff } ... }
Let’s assume waiter gets through the gate
first and begins executing waiter-Method( )
. The
two other threads are initially blocked, trying to acquire the lock
for the MyThing
object. When waiter executes the
wait( )
method, it relinquishes its hold on the
lock and goes to sleep. Now there are now two viable threads waiting
for the lock. Which thread gets it depends on several factors,
including chance and the priorities of the threads. (We’ll
discuss thread scheduling in the next section.)
Let’s say that notifier is the next thread
to acquire the lock, so it begins to run notifierMethod( )
. waiter continues to sleep and
related languishes, waiting for its turn. When
notifier executes the call to notify( )
, the runtime system prods the waiter
thread, effectively telling it something has changed.
waiter then wakes up and rejoins
related in vying for the
MyThing
lock. Note that it doesn’t receive
the lock automatically; it just changes from saying “leave me
alone” to “I want the lock.”
At this point, notifier still owns the lock and
continues to hold it until the synchronized notifierMethod( )
returns—or perhaps executes a wait( )
itself. At that point, the other two methods get to fight
over the lock. waiter would like to continue
executing waiterMethod( )
from the point it left
off, while related, which has been patient,
would like to get started. We’ll let you choose your own ending
for the story.
For each call
to notify( )
, the runtime system wakes up just one
method that is asleep in a wait( )
call. If there
are multiple threads waiting, Java picks a thread on an arbitrary
basis, which may be implementation-dependent. The
Object
class also provides a notifyAll( )
call to wake up all waiting threads. In most cases,
you’ll probably want to use notifyAll()
rather than notify()
. Keep in mind that
notify( )
really means “Hey, something
related to this object has changed. The condition you are waiting for
may have changed, so check it again.” In general, there is no
reason to assume only one thread at a time is interested in the
change or able to act upon it. Different threads might look upon
whatever has changed in different ways.
Often, our waiter thread is waiting for a particular condition to change and we will want to sit in a loop like the following:
while ( condition != true ) wait( );
Other synchronized threads call notify()
or
notifyAll( )
when they have modified the
environment so that waiter can check the
condition again. Using “wait conditions” like this is the
civilized alternative to polling and sleeping, as you’ll see in
the following
section.
Now
we’ll illustrate a classic interaction between two threads: a
Producer
and a Consumer
. A
producer thread creates messages and places them into a queue, while
a consumer reads them out and displays them. To be realistic,
we’ll give the queue a maximum depth. And to make things really
interesting, we’ll have our consumer thread be lazy and run
much more slowly than the producer. This means that
Producer
occasionally has to stop and wait for
Consumer
to catch up. Here are the
Producer
and Consumer
classes:
//file: Consumer.java import java.util.Vector; class Producer extends Thread { static final int MAXQUEUE = 5; private Vector messages = new Vector( ); public void run( ) { try { while ( true ) { putMessage( ); sleep( 1000 ); } } catch( InterruptedException e ) { } } private synchronized void putMessage( ) throws InterruptedException { while ( messages.size( ) == MAXQUEUE ) wait( ); messages.addElement( new java.util.Date().toString( ) ); notify( ); } // called by Consumer public synchronized String getMessage( ) throws InterruptedException { notify( ); while ( messages.size( ) == 0 ) wait( ); String message = (String)messages.firstElement( ); messages.removeElement( message ); return message; } } // end of class Producer public class Consumer extends Thread { Producer producer; Consumer(Producer p) { producer = p; } public void run( ) { try { while ( true ) { String message = producer.getMessage( ); System.out.println("Got message: " + message); sleep( 2000 ); } } catch( InterruptedException e ) { } } public static void main(String args[]) { Producer producer = new Producer( ); producer.start( ); new Consumer( producer ).start( ); } }
For convenience, we have included a main( )
method
in the Consumer
class that runs the complete
example. It creates a Consumer
that is tied to a
Producer
and starts the two classes. You can run
the example as follows:
% java Consumer
The output is the timestamp messages created by the
Producer
:
Got message: Sun Dec 19 03:35:55 CST 1999 Got message: Sun Dec 19 03:35:56 CST 1999 Got message: Sun Dec 19 03:35:57 CST 1999 ...
The timestamps initially show a spacing of one second, although they
appear every two seconds. Our Producer
runs faster
than our Consumer
. Producer
would like to generate a new message every second, while
Consumer
gets around to reading and displaying a
message only every two seconds. Can you see how long it will take the
message queue to fill up? What will happen when it does?
Let’s look at the code. We are using a few new tools here.
Producer
and Consumer
are
subclasses of Thread
. It would have been a better
design decision to have Producer
and
Consumer
implement the Runnable
interface, but we took the slightly easier path and subclassed
Thread
. You should find it fairly simple to use
the other technique; you might try it as an exercise.
The Producer
and Consumer
classes pass messages through an instance of a
java.util.Vector
object. We haven’t
discussed the Vector
class yet. Think of this one
as a queue: we add and remove elements in first-in, first-out order.
The
important activity is in the synchronized methods:
putMessage( )
and getMessage( )
. Although one of the methods is used by the
Producer
thread and the other by the
Consumer
thread, they both live in the
Producer
class so that we can coordinate them
simply by declaring them synchronized
. Here they
both implicitly use the Producer
object’s
lock. If the queue is empty, the Consumer
blocks
in a call in the Producer
, waiting for another
message.
Another design option would implement the getMessage( )
method in the Consumer
class and use a
synchronized
code block to synchronize explicitly
on the Producer
object. In either case,
synchronizing on the Producer
enables us to have
multiple Consumer
objects that feed on the same
Producer
. We’ll do that later in this
section.
putMessage( )
’s job is to add a new message
to the queue. It can’t do this if the queue is already full, so
it first checks the number of elements in
messages
. If there is room, it stuffs in another
timestamp message. If the queue is at its limit, however,
putMessage( )
has to wait until there’s
space. In this situation, putMessage()
executes a
wait()
and relies on the consumer to call
notify( )
to wake it up after a message has been
read. Here we have putMessage( )
testing the
condition in a loop. In this simple example, the test probably
isn’t necessary; we could assume that when putMessage( )
wakes up, there is a free spot. However, this test is
another example of good programming practice. Before it finishes,
putMessage( )
calls notify( )
itself to prod any Consumer
that might be waiting
on an empty queue.
getMessage( )
retrieves a message for the
Consumer
. It enters a loop like that of
putMessage( )
, waiting for the queue to have at
least one element before proceeding. If the queue is empty, it
executes a wait( )
and expects the
Producer
to call notify()
when
more items are available. Notice that getMessage( )
makes its own unconditional call to notify( )
. This is a somewhat lazy way of keeping the
Producer
on its toes, so that the queue should
generally be full. Alternatively, getMessage( )
might test to see if the queue had fallen below a low-water mark
before waking up the producer.
Now let’s add another consumer to the scenario, just to make
things really interesting. Most of the necessary changes are in the
Consumer
class; here’s the code for the
modified class, now called NamedConsumer
:
//file: NamedConsumer.java public class NamedConsumer extends Thread { Producer producer; String name; NamedConsumer(String name, Producer producer) { this.producer = producer; this.name = name; } public void run( ) { try { while ( true ) { String message = producer.getMessage( ); System.out.println(name + " got message: " + message); sleep( 2000 ); } } catch( InterruptedException e ) { } } public static void main(String args[]) { Producer producer = new Producer( ); producer.start( ); // start two this time new NamedConsumer( "One", producer ).start( ); new NamedConsumer( "Two", producer ).start( ); } }
The NamedConsumer
constructor takes a string name,
to identify each consumer. The run()
method uses
this name in the call to println( )
to identify
which consumer received the message.
The only
required modification to the Producer
code is to
change the notify( )
calls to
notifyAll()
calls in
putMessage()
and getMessage( )
.
(We could have used notifyAll( )
in the first
place.) Now, instead of the consumer and producer playing tag with
the queue, we can have many players waiting on the condition of the
queue to change. We might have a number of consumers waiting for a
message, or we might have the producer waiting for a consumer to take
a message. Whenever the condition of the queue changes, we prod all
of the waiting methods to reevaluate the situation by calling
notifyAll( )
.
Here is some sample output when there are two
NamedConsumers
running, as in the main( )
method shown previously:
One got message: Sat Mar 20 20:00:01 CST 1999 Two got message: Sat Mar 20 20:00:02 CST 1999 One got message: Sat Mar 20 20:00:03 CST 1999 Two got message: Sat Mar 20 20:00:04 CST 1999 One got message: Sat Mar 20 20:00:05 CST 1999 Two got message: Sat Mar 20 20:00:06 CST 1999 One got message: Sat Mar 20 20:00:07 CST 1999 Two got message: Sat Mar 20 20:00:08 CST 1999 ...
We see nice, orderly alternation between the two consumers, as a
result of the calls to sleep( )
in the various
methods. Interesting things would happen, however, if we were to
remove all of the calls to sleep( )
and let things
run at full speed. The threads would compete and their behavior would
depend on whether the system is using time-slicing. On a time-sliced
system, there should be a fairly random distribution between the two
consumers, while on a nontime-sliced system, a single consumer could
monopolize the messages. And since you’re probably wondering
about time-slicing, let’s talk about thread priority and
scheduling.
[26] Don’t confuse the term “serialize” in this context with Java object serialization, which is a mechanism for making objects persistent. But the underlying meaning (to place one thing after another) does apply to both. In the case of object serialization, it is the object’s data which is laid out, byte for byte, in a certain order.
[27] In actuality, they don’t really pass the lock around; the lock becomes available and, as we’ll describe, a thread that is scheduled to run acquires it.