All the sample
programs you developed in the preceding chapters have had only a single thread
of execution. Each program proceeded sequentially, one instruction after
another, until it completed its processing and terminated.
Multithreaded programs are similar to the single-threaded programs
that you have been studying. They differ only in the fact that they support
more than one concurrent thread of execution-that is, they are able to
simultaneously execute multiple sequences of instructions. Each instruction
sequence has its own unique flow of control that is independent of all others.
These independently executed instruction sequences are known as threads.
If your computer
has only a single CPU, you might be wondering how it can execute more than one
thread at the same time. In single-processor systems, only a single thread of
execution occurs at a given instant. The CPU quickly switches back and forth
between several threads to create the illusion that the threads are executing
at the same time. Single-processor systems support logical concurrency,
not physical concurrency. Logical concurrency is the characteristic
exhibited when multiple threads execute with separate, independent flows of
control. On multiprocessor systems, several threads do, in fact, execute at the
same time, and physical concurrency is achieved. The important feature of
multithreaded programs is that they support logical concurrency, not whether
physical concurrency is actually achieved.
Many programming
languages support multiprogramming. Multiprogramming is the logically
concurrent execution of multiple programs. For example, a program can request
that the operating system execute programs A, B, and C by having it spawn a
separate process for each program. These programs can run in parallel,
depending upon the multiprogramming features supported by the underlying
operating system. Multithreading differs from multiprogramming in that
multithreading provides concurrency within the context of a single process and
multiprogramming provides concurrency between processes. Threads are not
complete processes in and of themselves. They are a separate flow of control
that occurs within a process. Figure
8.1 illustrates the difference between multithreading and multiprogramming.
Figure
8.1 : Multithreading versus multiprogramming.
An executing
program is generally associated with a single process. The advantage of
multithreading is that concurrency can be used within a process to provide
multiple simultaneous services to the user. Multithreading also requires less
processing overhead than multiprogramming because concurrent threads are able
to share common resources more easily. Multiple executing programs tend to
duplicate resources and share data as the result of more time-consuming
interprocess communication.
Java's
multithreading support is centered around the java.lang.Thread class. The Thread class provides the capability to create objects of
class Thread, each with
its own separate flow of control. The Thread class encapsulates the data and methods associated
with separate threads of execution and allows multithreading to be integrated
within the object-oriented framework.
Java provides two
approaches to creating threads. In the first approach, you create a subclass of
class Thread and
override the run() method to
provide an entry point into the thread's execution. When you create an instance
of your Thread subclass,
you invoke its start() method to
cause the thread to execute as an independent sequence of instructions. The start() method is inherited from the Thread class. It initializes the Thread object using your operating system's multithreading
capabilities and invokes the run() method. You learn how to create threads using this
approach in the next section.
The approach to
creating threads identified in the previous paragraph is very simple and
straightforward. However, it has the drawback of requiring your Thread objects to be under the Thread class in the class hierarchy. In some cases, as
you'll see when you study applets in Part VI, "Programming the Web with
Applets and Scripts," this requirement can be somewhat limiting.
Java's other
approach to creating threads does not limit the location of your Thread objects within the class hierarchy. In this approach,
your class implements the java.lang.Runnable interface. The Runnable interface consists of a single method, the run() method, which must be overridden by your class. The run() method provides an entry point into your thread's
execution. In order to run an object of your class as an independent thread,
you pass it as an argument to a constructor of class Thread. You learn how to create threads using this approach
later in this chapter in the section titled "Implementing Runnable."
In this section,
you create your first multithreaded program by creating a subclass of Thread and then creating, initializing, and starting two Thread objects from your class. The threads will execute
concurrently and display Java is hot, aromatic, and invigorating. to the console window.
Listing 8.1. The source code
of the ThreadTest1
program.
class ThreadTest1
{
public static
void main(String args[])
{
MyThread thread1 = new
MyThread("thread1: ");
MyThread thread2 = new
MyThread("thread2: ");
thread1.start();
thread2.start();
boolean thread1IsAlive = true;
boolean thread2IsAlive = true;
do {
if (thread1IsAlive
&& !thread1.isAlive()) {
thread1IsAlive = false;
System.out.println("Thread 1
is dead.");
}
if (thread2IsAlive
&& !thread2.isAlive()) {
thread2IsAlive = false;
System.out.println("Thread 2 is dead.");
}
} while(thread1IsAlive ||
thread2IsAlive);
}
}
class MyThread extends Thread
{
static
String message[] =
{
"Java", "is",
"hot,", "aromatic,",
"and", "invigorating."};
public MyThread(String id)
{
super(id);
}
public void
run()
{
String name = getName();
for (int
i=0;i<message.length;++i) {
randomWait();
System.out.println(name +
message[i]);
}
}
void randomWait()
{
try {
sleep((long)(3000*Math.random()));
} catch (InterruptedException
x) {
System.out.println("Interrupted!");
}
}
}
This program
creates two threads of execution, thread1 and thread2, from the MyThread class. It then starts both threads and executes a do statement that waits for the threads to die. The
threads display the Java is hot, aromatic, and invigorating. message word by word, while waiting a short, random
amount of time between each word. Because both threads share the console
window, the program's output identifies which threads were able to write to the
console at various times during the program's execution.
Run ThreadTest1 to get an idea of the output that it produces. Each
time you run the program you might get a different program display. This is
because the program uses a random number generator to determine how long each
thread should wait before displaying its output. Look at the following output:
C:\java\jdg\ch08>java ThreadTest1
thread1: Java
thread2: Java
thread2: is
thread2: hot,
thread2: aromatic,
thread1: is
thread1: hot,
thread2: and
thread1: aromatic,
thread1: and
thread2: invigorating.
Thread 2 is dead.
thread1: invigorating.
Thread 1 is dead.
This output shows
that thread1 executed
first and displayed Java to the
console window. It then waited to execute while thread2 displayed Java, is, hot,, and aromatic,. After that, thread2 waited while thread1 continued its execution. thread1 displayed is and then hot,. At this point, thread2 took over again. thread2 displayed and and then went back into waiting. thread1 then displayed aromatic, and and. thread2
finished its execution by displaying invigorating.. Having completed its execution, thread2 died, leaving thread1 as the only executing task. thread1 displayed invigorating. and then completed its execution.
The ThreadTest1 class consists of a single main() method. This method begins by creating thread1 and thread2 as new objects of class MyThread. It then starts both threads using the start() method. At this point, main() enters a do loop that continues until both thread1 and thread2 are no longer alive. The loop monitors the execution
of the two threads and displays a message when it has detected the death of
each thread. It uses the isAlive()
method of the Thread class to
tell when a thread has died. The thread1IsAlive and thread2IsAlive variables are used to ensure that a thread's obituary
is only displayed once.
The MyThread class extends class Thread. It declares a statically initialized array, named message[], that contains the message to be displayed by each
thread. It has a single constructor that invokes the Thread class constructor via super(). It contains two access methods: run() and randomWait(). The run() method is required. It uses the getName() method of class Thread to get the name of the currently executing thread. It
then prints each word of the output display message while waiting a random
length of time between each print. The randomWait() method invokes the sleep() method within a try statement. The sleep() method is another method inherited from class Thread. It causes the currently executing task to "go
to sleep" or wait until a randomly specified number of milliseconds has
transpired. Because the sleep()
method throws the InterruptedException
when its sleep is interrupted (how grouchy!), the exception is caught and
handled by the randomWait()
method. The exception is handled by displaying the fact that an interruption
has occurred to the console window.
In the previous
section, you created a multithreaded program by creating the MyThread subclass of Thread. In this section, you create a program with similar
behavior, but you create your threads as objects of the class MyClass, which is not a subclass of Thread. MyClass
will implement the Runnable interface
and objects of MyClass will be
executed as threads by passing them as arguments to the Thread constructor.
The ThreadTest2 program's source code is shown in Listing 8.2. Enter
it into the ThreadTest2.java
file and compile it.
Listing 8.2. The source code
of the ThreadTest2
program.
class ThreadTest2
{
public static
void main(String args[])
{
Thread thread1 = new Thread(new MyClass("thread1:
"));
Thread thread2 = new Thread(new MyClass("thread2:
"));
thread1.start();
thread2.start();
boolean thread1IsAlive = true;
boolean thread2IsAlive = true;
do {
if (thread1IsAlive
&& !thread1.isAlive()) {
thread1IsAlive = false;
System.out.println("Thread 1 is dead.");
}
if (thread2IsAlive
&& !thread2.isAlive()) {
thread2IsAlive = false;
System.out.println("Thread 2 is dead.");
}
} while(thread1IsAlive ||
thread2IsAlive);
}
}
class MyClass implements Runnable
{
static
String message[] =
{
"Java", "is",
"hot,", "aromatic,",
"and", "invigorating."};
String name;
public MyClass(String id)
{
name = id;
}
public void
run()
{
for(int
i=0;i<message.length;++i) {
randomWait();
System.out.println(name+message[i]);
}
}
void randomWait()
{
try {
Thread.currentThread().sleep((long)(3000*Math.random()));
} catch (InterruptedException
x) {
System.out.println("Interrupted!");
}
}
}
The ThreadTest2 program is very similar to ThreadTest1. It differs only in the way that the threads are
created. You should run ThreadTest2
a few times to examine its output. Here are the results of a sample run I made
on my computer:
C:\java\jdg\ch08>java ThreadTest2
thread2: Java
thread1: Java
thread2: is
thread2: hot,
thread1: is
thread2: aromatic,
thread1: hot,
thread1: aromatic,
thread1: and
thread2: and
thread1: invigorating.
Thread 1 is dead.
thread2: invigorating.
Thread 2 is dead.
These results show
thread2 beginning its output before thread1. It does not mean that thread2 began executing before thread1. Thread1
executed first, but went to sleep before generating any output. Thread2 then executed and started its output display before
going to sleep. You can follow these results on your own to analyze how thread1 and thread2 switched back and forth during their execution to
display their results to the console window.
The main() method of ThreadTest2 differs from that of ThreadTest1 in the way that it creates thread1 and thread2. ThreadTest1
created the threads as new instances of the MyThread class. ThreadTest2 was not able to create the threads directly, because MyClass is not a subclass of Thread. Instead, ThreadTest2 first created instances of MyClass and then passed them to the Thread() constructor, creating instances of class Thread. The Thread() constructor used by ThreadTest2 takes as its argument any class that implements the Runnable interface. This is an example of the flexibility and
multiple-inheritance features provided by Java interfaces. The rest of the ThreadTest2 main()
method is the same as that of ThreadTest1.
MyClass
is declared as implementing the Runnable interface. This is a simple interface to implement;
it only requires that you implement the run() method. MyClass declares the name variable to hold the names of MyClass objects that are created. In the first example, the MyThread class did not need to do this because a thread-naming
capability was provided by Thread
and inherited by MyThread. MyClass contains a simple constructor that initializes the name variable.
The run() methods of ThreadTest2 and ThreadTest1 are nearly identical, differing only with respect to
the name issue. This is also true of the randomWait() method. In ThreadTest2, the randomWait() method must use the currentThread() method of class Thread to acquire a reference to an instance of the current
thread in order to invoke its sleep() method.
Because these two
examples are so similar, you might be wondering why you would pick one approach
to creating a class over another. The advantage of using the Runnable interface is that your class does not need to extend
the Thread class. This
will be very helpful feature when you start using multithreading in applets in
Part VI of this book. The only disadvantages to this approach are ones of convenience.
You have to do a little more work to create your threads and to access their
methods.
You have now
learned how to declare, create, initialize, start, and run Java threads. The ThreadTest1 and ThreadTest2 programs also introduced you to the concept of a
thread's death. Threads transition through several states from the time they
are created until the time of their death. This section reviews these states.
A thread is
created by creating a new object of class Thread or of one of its subclasses. When a thread is first
created, it does not exist as an independently executing set of instructions.
Instead, it is a template from which an executing thread will be created. It
first executes as a thread when it is started using the start() method and run via the run() method. Before a thread is started it is said to be
in the new thread state. After a thread is started, it is in the runnable
state. When a class is in the runnable state, it may be executing or
temporarily waiting to share processing resources with other threads. A
runnable thread enters an extended wait state when one of its methods is
invoked that causes it to drop from the runnable state into a not runnable
state. In the not runnable state, a thread is not just waiting for its
share of processing resources, but is blocked waiting for the occurrence of an
event that will send it back to the runnable state.
For example, the sleep() method was invoked in the ThreadTest1 and ThreadTest2 programs to cause a thread to wait for a short period
of time so that the other thread could execute. The sleep() method causes a thread to enter the not runnable
state until the specified time has expired. A thread may also enter the not
runnable state while it is waiting for I/O to be completed, or as the result of
the invocation of other methods.
A thread leaves
the not runnable state and returns to the runnable state when the event that it
is waiting for has occurred. For example, a sleeping thread must wait for its
specified sleep time to occur. A thread that is waiting on I/O must wait for
the I/O operation to be completed.
A thread may
transition from the new thread, runnable, or not runnable state to the dead
state when its stop() method is
invoked or the thread's execution is completed. When a thread enters the dead
state, it's a goner. It can't be revived and returned to any other state.
From an abstract
or a logical perspective, multiple threads execute as concurrent sequences of
instructions. This may be physically true for multiprocessor systems, under
certain conditions. However, in the general case, multiple threads do not
always physically execute at the same time. Instead, the threads share
execution time with each other based on the availability of the system's CPU
(or CPUs).
The approach used
to determining which threads should execute at a given time is referred to as scheduling.
Scheduling is performed by the Java runtime system. It schedules threads based
on their priority. The highest-priority thread that is in the runnable
state is the thread that is run at any given instant. The highest-priority
thread continues to run until it enters the death state, enters the not
runnable state, or has its priority lowered, or when a higher-priority thread becomes
runnable.
A thread's
priority is an integer value between MIN_PRIORITY and MAX_PRIORITY. These constants are defined in the Thread class. In Java 1.0, MIN_PRIORITY is 1 and MAX_PRIORITY is 10. A thread's priority is set when it is created.
It is set to the same priority as the thread that created it. The default
priority of a thread is NORM_PRIORITY and is equal to 5. The priority of a thread can be
changed using the setPriority()
method.
Java's approach to
scheduling is referred to as preemptive scheduling. When a thread of
higher priority becomes runnable, it preempts threads of lower priority and is
immediately executed in their place. If two or more higher-priority threads
become runnable, the Java scheduler alternates between them when allocating
execution time.
There are many
situations in which multiple threads must share access to common objects. For
example, all of the programs in this chapter have illustrated the effects of
multithreading by having multiple executing threads write to the Java console,
a common shared object. These examples have not required any coordination or
synchronization in the way the threads access the console window: Whatever
thread was currently executing was able to write to the console window. No coordination
between concurrent threads was required.
There are times
when you might want to coordinate access to shared resources. For example, in a
database system, you might not want one thread to be updating a database record
while another thread is trying to read it. Java enables you to coordinate the
actions of multiple threads using synchronized methods and synchronized
statements.
An object for
which access is to be coordinated is accessed through the use of synchronized
methods. These methods are declared with the synchronized keyword. Only one synchronized method can be invoked
for an object at a given point in time. This keeps synchronized methods in
multiple threads from conflicting with each other.
All
classes and objects are associated with a unique monitor. The monitor is
used to control the way in which synchronized methods are allowed to access the
class or object. When a synchronized method is invoked for a given object, it
is said to acquire the monitor for that object. No other synchronized
method may be invoked for that object until the monitor is released. A monitor
is automatically released when the method completes its execution and returns.
A monitor may also be released when a synchronized method executes certain
methods, such as wait(). The thread
associated with the currently executing synchronized method becomes not
runnable until the wait condition is satisfied and no other method has acquired
the object's monitor.
The following
example shows how synchronized methods and object monitors are used to
coordinate access to a common object by multiple threads. This example adapts
the ThreadTest1
program for use with synchronized methods, as shown in Listing 8.3.
Listing 8.3. The source code
of the ThreadSynchronization
program.
class ThreadSynchronization
{
public static
void main(String args[])
{
MyThread thread1 = new
MyThread("thread1: ");
MyThread thread2 = new
MyThread("thread2: ");
thread1.start();
thread2.start();
boolean thread1IsAlive = true;
boolean thread2IsAlive = true;
do {
if (thread1IsAlive
&& !thread1.isAlive()) {
thread1IsAlive = false;
System.out.println("Thread 1 is dead.");
}
if (thread2IsAlive
&& !thread2.isAlive()) {
thread2IsAlive = false;
System.out.println("Thread 2 is dead.");
}
} while(thread1IsAlive ||
thread2IsAlive);
}
}
class MyThread extends Thread
{
static
String message[] =
{
"Java", "is",
"hot,", "aromatic,",
"and", "invigorating."};
public MyThread(String id)
{
super(id);
}
public void
run()
{
SynchronizedOutput.displayList(getName(),message);
}
void randomWait()
{
try {
sleep((long)(3000*Math.random()));
} catch (InterruptedException
x) {
System.out.println("Interrupted!");
}
}
}
class SynchronizedOutput
{
public
static synchronized void
displayList(String name,String list[])
{
for(int i=0;i<list.length;++i)
{
MyThread
t = (MyThread) Thread.currentThread();
t.randomWait();
System.out.println(name+list[i]);
}
}
}
Compile and run the program before going on
with its analysis. You might be surprised at the results that you've obtained.
Here are the results of an example run on my system:
C:\java\jdg\ch08>java ThreadSynchronization
thread1: Java
thread1: is
thread1: hot,
thread1: aromatic,
thread1: and
thread1: invigorating.
Thread 1 is dead.
thread2: Java
thread2: is
thread2: hot,
thread2: aromatic,
thread2: and
thread2: invigorating.
Thread 2 is dead.
Now edit ThreadSynchronization.java and delete the synchronized
keyword in the declaration of the displayList() method of class SynchronizedOutput.
Save ThreadSynchronization.java,
recompile it, and rerun it with the new change in place. You may now get output
similar to this:
C:\java\jdg\ch08>java ThreadSynchronization
thread2: Java
thread1: Java
thread1: is
thread2: is
thread2: hot,
thread2: aromatic,
thread1: hot,
thread2: and
thread2: invigorating.
Thread 2 is dead.
thread1: aromatic,
thread1: and
thread1: invigorating.
Thread 1 is dead.
The difference in
the program's output should give you a feel for the effects of synchronization
upon multithreaded program execution. Let's analyze the program and explain
these results.
The ThreadSynchronization class is essentially the same as the ThreadTest1 class. The only difference is the class name.
The MyThread class was modified slightly to allow for the use of
the SynchronizedOutput
class. Instead of the output being displayed in the run() method, as in ThreadTest1, the run() method simply invokes the displayList() method of the SynchronizedOutput class. It is important to understand that the displayList() method is static and applies to the SynchronizedOutput class as a whole, not to any particular instance of
the class. The method displays the Java is hot, aromatic, and
invigorating. message in the same
manner as it was displayed in the previous examples of this chapter. It invokes
randomWait() to wait a random amount of time before displaying
each word in the message. The displayList() method uses the currentThread() method of class Thread to reference the current thread in order to invoke randomWait().
What difference,
then, does the fact that displayList() is synchronized have on the program's execution? When
displayList() is not synchronized, it may be invoked by one thread,
say thread1, display
some output, and wait while thread2 executes. When thread2 executes, it too invokes displayList() to display some output. Two separate invocations of displayList(), one for thread1 and the other for thread2, execute concurrently. This explains the mixed output
display.
When the synchronized keyword is used, thread1 invokes displayList(), acquires a monitor for the SynchronizedOutput class (because displayList() is a static method), and displayList() proceeds with the output display for thread1. Because thread1 acquired a monitor for the SynchronizedOutput class, thread2 must wait until the monitor is released before it is
able to invoke displayList() to
display its output. This explains why one task's output is completed before the
other's.
Java borrows the
notion of a daemon thread from the UNIX daemon process. A daemon thread
is a thread that executes in the background and provides services to other
threads. It typically executes a continuous loop of instructions that wait for
a service request, perform the service, and wait for the next service request.
Daemon threads continue to execute until there are no more threads for which
services can be provided. At this time, the daemon threads die and the Java
interpreter terminates its execution. Any thread can be changed to a daemon
thread using the setDaemon()
method.
Thread groups are objects that consist of a collection of threads.
Every thread is a member of a unique thread group. Thread groups are used to
invoke methods that apply to all threads in the group. For example, a thread
group can be used to start or stop all threads in a group, to change their
priorities, or to change them to daemon threads.
A thread is
entered into a thread group when it is created. After the thread enters a thread
group, it remains a member of the group throughout its existence. A thread can
never become a member of another group.
Threads are entered into a group
using Thread
constructors that take a ThreadGroup
parameter. These constructors are described in the Thread class API documentation.
If a thread's group is not specified in its constructor, as is the usual case,
the thread is entered into the same group as the thread that created it. The
default thread group for a newly executing Java application is the main group.
A Thread continues to execute until one of
the following things happens:
So what happens if the run() method for
a thread never terminates, and the application that started the thread never
calls its stop()
method? The answer is that the thread lives on, even after the application that
created it has finished. This means we have to be aware of how our threads
eventually terminate, or an application can end up leaving orphaned threads
that unnecessarily consume resources.
In many cases, what we really want is
to create background threads that do simple, periodic tasks in an application.
The setDaemon()
method can be used to mark a Thread as a daemon thread that should be killed and
discarded when no other application threads remain. Normally, the Java
interpreter continues to run until all threads have completed. But when daemon
threads are the only threads still alive, the interpreter will exit.
Here's a devilish example of using
daemon threads:
class Devil extends Thread {
Devil() {
setDaemon( true );
start();
}
public void
run() {
// Perform evil tasks
...
}
}
In
the above example, the Devil
thread sets its daemon status when it is created. If any Devil threads
remain when our application is otherwise complete, Java kills them for us. We
don't have to worry about cleaning them up.
Daemon
threads are primarily useful in standalone Java applications and in the
implementation of the Java system itself, but not in applets. Since an applet
runs inside of another Java application, any daemon threads it creates will
continue to live until the controlling application exits--probably not the
desired effect.
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 public
restroom at a gas station. 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, just as if 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, namely an object. In other words, synchronization makes sure only one
thread at a time can perform certain activities that manipulate an object. 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 was 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
...
}
The
code block above 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 have methods (or parts of methods) in
different classes synchronized the same as methods in the same class.
A
synchronized 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 )
{
...
}
}
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 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 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 you'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, respectively. Here's a code fragment to illustrate:
class MyThing
{
synchronized void waiterMethod()
{
// Do some stuff
// Now we need to wait for notifier
to do something
wait();
// Continue where we left off
}
synchronized void
notifierMethod()
{
// Do some stuff
// Notify waiter that we've done it
notify();
// Do more things
}
synchronized void relatedMethod()
{
// Do some related stuff
}
}
Let's
assume waiter
gets through the gate first and begins executing waiterMethod(). 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. waiter continues
to sleep and related
languishes, waiting for its turn. When notifier executes the call to notify(), Java
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 actually receive the lock; 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 it leaves its synchronized
method (or perhaps executes a wait() itself ). When it finally completes, the
other two methods get to fight over the lock. waiter would like to continue executing waiterMethod()
from the point it left off, while unrelated, 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(),
Java wakes up just one method that is asleep in a wait() call. If there are
multiple threads waiting, Java picks the first thread on a first-in, first-out
basis. 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. This is the civilized alternative to polling and sleeping, as you'll see
the following example.
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 slower than the producer. This means that Producer
occasionally has to stop and wait for Consumer to catch up. The example below
shows the Producer
and Consumer
classes.
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;
}
}
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 that runs the complete example in the Consumer class. 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 time-stamp messages created by the Producer:
Got message: Sun Dec 19 03:35:55 CST 1996
Got message: Sun Dec 19 03:35:56 CST 1996
Got message: Sun Dec 19 03:35:57 CST 1996
...
The
time stamps 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, but you can think of this one as a queue where 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 because they have to be synchronized on the same object to work together.
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 explicitly synchronize on the Producer object.
In either case, synchronizing on the Producer is important because it allows
us to have multiple Consumer
objects that feed on the same Producer.
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 time stamp. 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 the Producer's,
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; the example below shows the code for the modified class.
class Consumer extends Thread
{
Producer
producer;
String name;
Consumer(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 Consumer( "One", producer ).start();
new Consumer( "Two", producer ).start();
}
}
The
Consumer
constructor now 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 modification to make in the Producer code is to change the call to notify() in putMessage() to
a call to notifyAll().
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().
Note, however, that we don't need to change the call to notify() in getMessage(). If
a Consumer
thread is waiting for a message to appear in the queue, it's not possible for
the Producer
to be simultaneously waiting because the queue is full.
Here
is some sample output when there are two consumers running, as in the main() method
shown above:
One got message: Wed Mar 20 20:00:01 CST 1996
Two got message: Wed Mar 20 20:00:02 CST 1996
One got message: Wed Mar 20 20:00:03 CST 1996
Two got message: Wed Mar 20 20:00:04 CST 1996
One got message: Wed Mar 20 20:00:05 CST 1996
Two got message: Wed Mar 20 20:00:06 CST 1996
One got message: Wed Mar 20 20:00:07 CST 1996
Two got message: Wed Mar 20 20:00:08 CST 1996
...
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 or not 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.