Every thread has a mind 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 that they can work together and not collide in their use of the same variables and methods (coordinating their play on the golf course).
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. 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 very much like a restroom with a lock on the door; if it’s unlocked, you can enter and lock the door while you are using it. If the resource is not being used, the thread can acquire the lock and access the resource. When the thread is done, it relinquishes the lock, just as you unlock the restroom door and leave it open for the next person. However, if another thread already has the lock for the resource, all other threads must wait until the current thread is done and has released the lock. This is just like when the restroom is occupied when you arrive: you have to wait until the current user is done and unlocks the door.
Fortunately, Java makes the process of synchronizing access to resources fairly easy. The language handles setting up and acquiring locks; all you need to do is specify the resources that require synchronization.
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 an associated lock. 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, suppose we implemented a SpeechSynthesizer
class that contains a
say()
method. We don’t want multiple
threads calling say()
at the same
time because we wouldn’t be able to understand anything being said. So
we mark the say()
method as synchronized
, which means that a thread must
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 must acquire the lock on the SpeechSynthesizer
instance it’s 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. It doesn’t matter whether the thread is owned by the
SpeechSynthesizer
itself or some
other object; every thread must 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
. In this case, because no
instance object is involved, the lock is on the class object
itself.
Often, you want to synchronize multiple methods of the same class
so that only one method 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, methods setRow()
and sumRow()
both 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 was reading the values in sumRow()
. To prevent this, we have marked both
methods as synchronized
. When threads
are synchronized, only one runs 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 finishes executing setRow()
before it runs sumRow()
. This synchronization allows us to
preserve the consistency of the SpreadSheet
. The best part is that all this
locking and waiting is handled by Java; it’s invisible 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 exclusive access to resources
}
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
()
{
...
}
void
myMethod
()
{
synchronized
(
this
)
{
...
}
}
In the SpreadSheet
example, we guarded access to a set of instance variables with a
synchronized method in order to avoid changing one of the variables
while someone was reading the others. 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: that is, they are handled by the VM in
one step, with no opportunity for two threads to collide. This prevents
threads from looking at references while they are in the process of
being accessed by other threads.
But watch out—we did say almost. If you read
the Java VM 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 VM’s
stack handles them. It is possible that this specification will be
beefed up in the future. But for now, to be strict, you should
synchronize access to your double
and
long
instance variables through
accessor methods, or use the volatile
keyword or an
atomic wrapper class, which we’ll describe next.
Another issue, independent of the atomicity of the values, is the
notion of different threads in the VM caching values for periods of
time—that is, even though one thread may have changed the value, the
Java VM may not be obliged to make that value appear until the VM
reaches a certain state known as a “memory barrier.” While this should
not be a problem in most real-world programming cases, you can address
this by declaring the variable with the volatile
keyword. This keyword indicates to
the VM that the value may be changed by external threads and effectively
synchronizes access to it automatically.
Finally, the java.util.concurrent.atomic
package provides
synchronized wrapper classes for all primitive types and references.
These wrappers provide not only simple set()
and get()
operations on the values but also
specialized “combo” operations, such as compareAndSet()
, that
work atomically and can be used to build higher-level synchronized
application components. The classes in this package were designed
specifically to map down to hardware-level functionality in many cases
and can be very efficient. We’ll talk more about them later in this
chapter.
The locks acquired by Java upon entering a synchronized method or block of code are reentrant, meaning that the thread holding onto the lock may acquire the same lock again any number of times and never blocks waiting for itself. In most cases, this means that the code behaves as you’d expect; a thread can call a synchronized method recursively and can itself call upon other synchronized methods within the same object.
With the synchronized
keyword, we can serialize the execution of methods and blocks of code so
that only one thread at a time can execute a synchronized item. The
wait()
and notify()
methods of the Object
class extend
this capability by allowing us to explicitly coordinate the waiting and
running threads. 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. All of the coordinated activity still
happens inside 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 running thread calls notify()
from a block synchronized on the same
object. 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 where it left off. However, the thread that was waiting 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. The first thread won’t wake up from the wait()
unless another thread calls notify()
. An overloaded version of wait()
, however, 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
// continue doing stuff
}
synchronized
void
relatedMethod
()
{
// do some related stuff
}
...
}
Let’s assume that a thread named waiter gets
through the gate first and begins executing waiterMethod()
. The two other threads are
initially blocked when 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 two viable threads are 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 suppose 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 wakes up and rejoins
related in vying for the MyThing
lock. It doesn’t receive the lock
automatically; it just changes its state from “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 where 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 thread that is asleep in a wait()
call. The group of threads waiting on a
lock is called the wait set. If multiple threads
are 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.
In general, our waiter thread is waiting for a particular condition to change, and we will want it to sit in a loop like the following:
while
(
condition
!=
true
)
wait
();
This test is called the wait condition.
Other synchronized threads call notify()
or notifyAll()
when they
have modified the environment so that the condition can be checked
again. It’s important to use a loop on the wait condition to be sure
that the thread has been awakened for the right reason. Threads may
also use a timed version of wait()
to do periodic work while checking the condition in this way. Using
wait conditions like this is also an alternative to polling and
sleeping, as you’ll see in the following section.
We’ll next 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 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. The Java
concurrency package has a BlockingQueue
interface
that provides exactly this kind of functionality, but we’ll build it
ourselves here using basic synchronization techniques first and then
take a look at Queue
s and all of the
collection classes in Chapter 11.
Here are the Producer
and
Consumer
classes:
import
java.util.*
;
public
class
Consumer
implements
Runnable
{
Producer
producer
;
Consumer
(
Producer
producer
)
{
this
.
producer
=
producer
;
}
public
void
run
()
{
while
(
true
)
{
String
message
=
producer
.
getMessage
();
System
.
out
.
println
(
"Got message: "
+
message
);
try
{
Thread
.
sleep
(
2000
);
}
catch
(
InterruptedException
e
)
{
}
}
}
public
static
void
main
(
String
args
[])
{
Producer
producer
=
new
Producer
();
new
Thread
(
producer
).
start
();
Consumer
consumer
=
new
Consumer
(
producer
);
new
Thread
(
consumer
).
start
();
}
}
public
class
Producer
implements
Runnable
{
static
final
int
MAXQUEUE
=
5
;
private
List
messages
=
new
ArrayList
();
public
void
run
()
{
while
(
true
)
{
putMessage
();
try
{
Thread
.
sleep
(
1000
);
}
catch
(
InterruptedException
e
)
{
}
}
}
// called by Producer internally
private
synchronized
void
putMessage
()
{
while
(
messages
.
size
()
>=
MAXQUEUE
)
try
{
wait
();
}
catch
(
InterruptedException
e
)
{
}
messages
.
add
(
new
java
.
util
.
Date
().
toString
()
);
notify
();
}
// called by Consumer externally
public
synchronized
String
getMessage
()
{
while
(
messages
.
size
()
==
0
)
try
{
notify
();
wait
();
}
catch
(
InterruptedException
e
)
{
}
String
message
=
(
String
)
messages
.
remove
(
0
);
notify
();
return
message
;
}
}
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
This produces the timestamp messages created by the Producer
:
Got
message:
Sun
Dec
19
03
:
35
:
55
CST
2006
Got
message:
Sun
Dec
19
03
:
35
:
56
CST
2006
Got
message:
Sun
Dec
19
03
:
35
:
57
CST
2006
...
The timestamps initially show a spacing of one second even though
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 happens
when it does?
Let’s look at the code. We are using a few new tools here.
Producer
and Consumer
implement the Runnable
interface, and each has a thread
associated with it. The Producer
and
Consumer
classes pass messages
through an instance of a java.util.List
object. We haven’t discussed
the List
class yet, but it is
essentially a dynamic array of elements. We use this one as a queue by
simply adding and removing elements in first-in, first-out order. The
List
has no maximum capacity of its
own, but we impose one with our own check.
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 from 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 might not seem necessary; we could
assume that when putMessage()
wakes
up, there is a free spot. However, it’s important to always test our
wait condition in a loop like this when we synchronize threads because
there is no other way to be certain why our thread has been awakened.
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 calls to notify()
. It does this any time the queue is
empty, to prod a producer that might be sleeping and also after it
consumes a message, to give the producer the go-ahead to fill the queue
again. These scenarios are more plausible if there are more consumers,
as we’ll see next.
Let’s add another consumer to the scenario, just to make things
more interesting. Most of the necessary changes are in the Consumer
class; here’s the code for the
modified class, now called NamedConsumer
:
public
class
NamedConsumer
implements
Runnable
{
Producer
producer
;
String
name
;
NamedConsumer
(
String
name
,
Producer
producer
)
{
this
.
producer
=
producer
;
this
.
name
=
name
;
}
public
void
run
()
{
while
(
true
)
{
String
message
=
producer
.
getMessage
();
System
.
out
.
println
(
name
+
" got message: "
+
message
);
try
{
Thread
.
sleep
(
2000
);
}
catch
(
InterruptedException
e
)
{
}
}
}
public
static
void
main
(
String
args
[])
{
Producer
producer
=
new
Producer
();
new
Thread
(
producer
).
start
();
NamedConsumer
consumer
=
new
NamedConsumer
(
"One"
,
producer
);
new
Thread
(
consumer
).
start
();
consumer
=
new
NamedConsumer
(
"Two"
,
producer
);
new
Thread
(
consumer
).
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 for 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. Any time 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 two NamedConsumers
are running, as in the main()
method shown previously:
One
got
message:
Sat
Mar
18
20
:
00
:
01
CST
2006
Two
got
message:
Sat
Mar
18
20
:
00
:
02
CST
2006
One
got
message:
Sat
Mar
18
20
:
00
:
03
CST
2006
Two
got
message:
Sat
Mar
18
20
:
00
:
04
CST
2006
One
got
message:
Sat
Mar
18
20
:
00
:
05
CST
2006
Two
got
message:
Sat
Mar
18
20
:
00
:
06
CST
2006
One
got
message:
Sat
Mar
18
20
:
00
:
07
CST
2006
Two
got
message:
Sat
Mar
18
20
:
00
:
08
CST
2006
...
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 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 non-time-sliced system, a single consumer could
monopolize the messages. We’ll talk shortly about how threads compete
for time when we discuss thread priority and scheduling.
Many things could be improved in this simple example. What we’ve
tried to emphasize is a defensive style of programming with respect to
notifications by threads. You need to rely on real-world conditions
that you can test when synchronizing threads; it’s not robust to
simply assume that you’ll get the right notifications in the right
place at the right time. With that said, our example does generate
extraneous notifications that wake up threads at times when there may
not be work for them. For example, we generate notifications both when
the queue is empty and when it’s full. A better design might split
these cases and use two different object locks. Fortunately, most
programmers won’t have to deal with issues at this level, especially
because Java provides real Queue
s
and other high-level synchronization constructs.
A common issue that arises is the need to maintain some
information or state on a per-thread basis. For example, we might want
to carry some context with the current thread as it executes our
application. Or we might simply want to have a value that is different
for different threads in the same way that each thread “sees” its own
local variables in a method. Java supports this through the ThreadLocal
class. A ThreadLocal
is an object wrapper that
automatically maintains a separate value for any thread calling it. For
example:
ThreadLocal
userID
=
new
ThreadLocal
();
userID
.
set
(
"Pat"
);
// called by thread 1
userID
.
set
(
"Bob"
);
// called by thread 2
userID
.
get
();
// thread 1 gets "Pat"
userID
.
get
();
// thread 2 gets "Bob"
You can use an instance of ThreadLocal
anywhere you might use a static or
instance variable to automatically maintain separate values for each
thread. You can also extend ThreadLocal
and override its initialValue()
method. The ThreadLocal
will then use this method to
initialize its value once, the first time get()
is called:
class
MyThreadLocalFactory
extends
ThreadLocal
<
Factory
>
{
protected
Factory
initialValue
()
{
return
new
MyFactory
();
}
}
ThreadLocal
s are implemented
using a Map
attached to each Thread
instance, so their values will
disappear when the Thread
is no
longer used and garbage is collected.
A useful addition in Java 7 is the ThreadLocalRandom
class, which is an extension of the java.util.Random
class
discussed in Chapter 11. The ThreadLocalRandom
class eliminates contention
(waiting due to synchronization) on the random-number generator when
called from different threads.
[26] Don’t confuse the term serialize in this context with Java object serialization, which is a mechanism for making objects persistent. The underlying meaning (to place one thing after another) does apply to both, however. In the case of object serialization, the object’s data is laid out, byte for byte, in a certain order.
Get Learning Java, 4th Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.