Java 1.1 Unleashed
- 6 -
Threads and Multithreading
by Eric Williams
IN THIS CHAPTER
- Multithreading
- Concurrency
- Advanced Monitor Concepts
- Synchronization
Multithreading
One of the characteristics that makes Java a powerful programming language is
its support for multithreaded programming as an integrated part of the language.
This provision is unique because most modern programming languages either do not
offer multithreading or provide multithreading as a nonintegrated package. Java,
however, offers a single, integrated view of multithreading.
Multithreaded programming is an essential aspect of programming in Java. To master
the Java programming language, you should first become familiar with the concepts
of multithreaded programming. Then you should learn how multithreaded and concurrent
programming are done in Java.
This chapter presents a complete introduction and reference to Java threads, including
these topics:
- How to write and start your own threads
- A comprehensive reference to the Thread and ThreadGroup classes
- How to make your classes thread safe
- An introduction to Java monitors
- How to coordinate the actions of multiple threads
NOTE: Multithreading and concurrent programming
are unfamiliar concepts for most new Java programmers. If you are familiar with only
single-threaded languages like Visual Basic, Delphi, Pascal, Cobol, and so on, you
may be worried that threads are too hard to learn. Although learning to use Java
threads is not trivial, the model is simple and easy to understand. Threads are a
normal everyday aspect of developing Java applications and applets.
What Is a Thread?
In the early days of computing, computers were single tasking--that is, they ran
a single job at a time. The big, lumbering machine would start one job, run that
job to completion, then start the next job, and so on. When engineers became overly
frustrated with these batch-oriented systems, they rewrote the programs that ran
the machines and thus was born the modern multitasking operating system.
Multitasking refers to a computer's capability to perform multiple jobs concurrently.
For the most part, modern operating systems like Windows 95 or Solaris can run two
or more programs at the same time. While you are using Netscape to download a big
file, you can be running Solitaire in a different window; both programs are running
at the same time.
Multithreading is an extension of the multitasking paradigm. But rather than multiple
programs, multithreading involves multiple threads of control within a single program.
Not only is the operating system running multiple programs, each program can run
multiple threads of control--think of threads as subprograms--within the program.
For example, using a Web browser, you can print one Web page, download another, and
fill out a form in a third--all at the same time.
A thread is a single sequence of execution within a program. Until now, you have
probably used Java to write single-threaded applications, something like this:
class MainIsRunInAThread {
public static void main(String[] args) {
// main() is run in a single thread
System.out.println(Thread.currentThread());
for (int i=0; i<1000; i++) {
System.out.println("i == " + i);
}
}
}
This example is simplistic, but it demonstrates the use of a single Java thread.
When a Java application begins, the virtual machine (VM) runs the main()
method inside a Java thread. (You have already used Java threads and didn't even
know it!) Within this single thread, this simple application's main() method
counts from 0 to 999, printing out each value as it is counted.
Programming within a single sequence of control can limit your ability to produce
usable Java software. (Imagine using an operating system that could execute only
one program at a time, or a Web browser that could load only a single page at a time.)
When you write a program, you often want the program to do multiple things at the
same time. For example, you may want the program to retrieve an image over the network
at the same time it is requesting an updated stock report and also running several
animations--and you want all this to occur concurrently. This is the kind of situation
in which Java threads become useful.
Java threads allow you to write programs that do many things at once. Each thread
represents an independently executing sequence of control. One thread can write a
file out to disk while a different thread responds to user keystroke events.
Before jumping into the details about Java threads, let's take a peek at what
a multithreaded application looks like. Listing 6.1 modifies the preceding single-threaded
application to take advantage of threads. Instead of counting from 0 to 999 in one
thread, this application uses five different threads to count from 0 to 999--each
thread counts 200 numbers: 0 to 199, 200 to 399, and so on. Don't worry if you don't
understand the details of this example yet; it is presented only to introduce you
to threads.
Listing 6.1. A simple multithreaded application.
class CountThreadTest extends Thread {
int from, to;
public CountThreadTest(int from, int to) {
this.from = from;
this.to = to;
}
// the run() method is like main() for a thread
public void run() {
for (int i=from; i<to; i++) {
System.out.println("i == " + i);
}
}
public static void main(String[] args) {
// spawn 5 threads, each of wich counts 200 numbers
for (int i=0; i<5; i++) {
CountThreadTest t = new CountThreadTest(i*200, (i+1)*200);
// starting a thread will launch a separate sequence
// of control and execute the run() method of the thread
t.start();
}
}
}
When this application starts, the VM invokes the main() method in its
own thread. main() then starts five separate threads to perform the counting
operations. Figure 6.1 shows the threads in the CountThreadTest application.
Figure 6.1.
Parallel Java threads.
NOTE: Even though multiple threads may
appear to perform tasks at the same time, technically speaking, this may not be true.
Even today, most computers are equipped with a single processor--such computers can
perform at most one task at a time. On single-processor systems, the operating system
continuously switches between different tasks and threads, allowing each active task
or thread to use the CPU for a small amount of time. This subject is discussed in
detail in "Thread Scheduling," later in this chapter.
Java Threads
Support for multiple threads of execution is not a Java invention. Threads have
been around for a long time and have been implemented in many programming languages.
However, programmers have had to struggle with a lack of thread standards. Different
platforms have different thread packages, each with a different API. Operating systems
do not have uniform support for threads; some support threads in the OS kernel, and
some do not. Only recently has a standard emerged for threads--POSIX threads (IEEE
standard 1003.1c-1995). However, the POSIX threads standard defines only a C programming
interface (not a Java interface) and is not yet widely implemented.
One of the greatest benefits of Java is that it presents the Java programmer with
a unified multithreading API--one that is supported by all Java virtual machines
on all platforms. When you use Java threads, you do not have to worry about which
threading packages are available on the underlying platform or whether the operating
system supports kernel threads. The virtual machine isolates you from the platform-specific
threading details. The Java threading API is identical on all Java implementations.
Creating New Threads
The first thing you need to know about threads is how to create and run a thread.
This process involves two steps: writing the code that is executed in the thread
and writing the code that starts the thread.
As discussed earlier, you are already familiar with how to write single-threaded
programs. When you write a main() function, that method is executed in a
single thread. The Java virtual machine provides a multithreaded environment, but
it starts user applications by calling main() in a single thread.
An application's main() method provides the central logic for the main
thread of the application. Writing the code for a thread is similar to writing main().
You must provide a method that implements the main logic of the thread. This method
is always named run() and has the following signature:
public void run();
Notice that the run() method is not a static method as main()
is. The main() method is static because an application starts with only
one main() method. But an application may have many threads, so the main
logic for a thread is associated with an object--the Thread object.
You can provide an implementation for the run() method in two ways. Java
supports the run() method in subclasses of the Thread class. Java
also supports run() through the Runnable interface. Both methods
for providing a run() method implementation are described in the following
sections.
Subclassing the Thread Class
This section discusses how to create a new thread by subclassing java.lang.Thread.
Let's start with a plausible situation in which a thread might be useful. Suppose
that you are building an application; in one part of this application, a file must
be copied from one directory to a different directory. But when you run the application,
you find that if the file is large, the application stalls during the time that the
file is being copied. You determine that the cause of the stall is this: When the
application is copying the file, it is unable to respond to user-interface events.
To improve this situation, you decide that the file-copy operation should be performed
concurrently, in a separate thread. To move this logic to a thread, you provide a
subclass of the Thread class that contains this logic, implemented in the
run() method. The FileCopyThread class shown in Listing 6.2 contains
this logic.
Listing 6.2. The file-copy logic in FileCopyThread.
// subclass from Thread to provide your own kind of Thread
class FileCopyThread extends Thread {
private File from;
private File to;
public FileCopyThread(File from, File to) {
this.from = from;
this.to = to;
}
// implement the main logic of the thread in the run()
// method [run() is equivalent to an application's main()]
public void run() {
FileInputStream in = null;
FileOutputStream out = null;
byte[] buffer = new byte[512];
int size = 0;
try {
// open the input and output streams
in = new FileInputStream(from);
out = new FileOutputStream(to);
// copy 512 bytes at a time until EOF
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
} catch(IOException ex) {
ex.printStackTrace();
} finally {
// close the input and output streams
try {
if (in != null) { in.close(); }
if (out != null) { out.close(); }
} catch (IOException ex) {
}
}
}
}
Let's analyze the FileCopyThread class. Note that the FileCopyThread
subclasses from Thread. By subclassing from Thread, FileCopyThread
inherits all the state and behavior of a Thread--the property of "being
a thread."
The FileCopyThread class implements the main logic of the thread in the
run() method. (Remember that the run() method is the initial method
for a Java thread, just as the main() method is the initial method for a
Java application.) Within run(), the input file is copied to the output
file in 512-byte chunks. When a FileCopyThread instance is created and started,
the entire run() method is executed in one separate sequence of control
(you'll see how this is done soon).
Now that you are familiar with how to write a Thread subclass, you have
to learn how to use that class as a separate control sequence within a program. To
use a thread, you must start the concurrent execution of the thread by calling the
Thread object's start() method. The following code demonstrates
how to launch a file-copy operation as a separate thread:
File from = getCopyFrom();
File to = getCopyTo();
// create an instance of the thread class
Thread t = new FileCopyThread(from, to);
// call start() to activate the thread asynchronously
t.start();
Invoking the start() method of a FileCopyThread object begins
the concurrent execution of that thread. When the thread starts running, its run()
method is called. In this case, the file copy begins its execution concurrently with
the original thread. When the file copy is finished, the run() method returns
(and the concurrent execution of the thread ends). This process is shown in Figure
6.2.
Figure 6.2.
Concurrent file copy.
Implementing the Runnable Interface
There are situations in which it is not convenient to create a Thread
subclass. For example, you may want to add a run() method to a preexisting
class that does not inherit from Thread. The Java Runnable interface
makes this possible.
The Java threading API supports the notion of a thread-like entity that is an
interface: java.lang.Runnable. Runnable is a simple interface,
with only one method:
public interface Runnable {
public void run();
}
This interface should look familiar. In the previous section, we covered the Thread
class, which also supported the run() method. To subclass Thread,
we redefined the Thread run() method. To use the Runnable
interface, you must write a run() method and add the text implements
Runnable to the class. Reimplementing the FileCopyThread (of the previous
example) as a Runnable interface requires few changes:
// implementing Runnable is a different way to use threads
class FileCopyRunnable implements Runnable {
// the rest of the class remains mostly the same
...
}
To use a Runnable interface as a separate control sequence requires the
cooperation of a Thread object. Although the Runnable object contains
the main logic, Thread is the only class that encapsulates the mechanism
of launching and controlling a thread. To support Runnable, a separate Runnable
parameter was added to several of the Thread class constructors. A thread
that has been initialized with a Runnable object will call that object's
run() method when the thread begins executing.
Here is an example of how to start a thread using FileCopyRunnable:
File from = new File("file.1");
File to = new File("file.2");
// create an instance of the Runnable
Runnable r = new FileCopyRunnable(from, to);
// create an instance of Thread, passing it the Runnable
Thread t = new Thread(r);
// start the thread
t.start();
Thread States
Although you have learned a few things about threads, we have not yet discussed
one aspect that is critical to your understanding of how threads work in Java--thread
states. A Java thread, represented by a Thread object, traverses a fixed
set of states during its lifetime (see Figure 6.3).
Figure 6.3.
Thread states.
When a Thread object is first created, it is in the NEW state.
At this point, the thread is not executing. When you invoke the Thread's
start() method, the thread changes to the RUNNABLE state.
When a Java thread is RUNNABLE, it is eligible for execution. However,
a thread that is RUNNABLE is not necessarily running. RUNNABLE
implies that the thread is alive and that it can be allocated CPU time by the system
when the CPU is available--but the CPU may not always be available. On single-processor
systems, Java threads must share the single CPU; additionally, the Java virtual machine
task (or process) must also share the CPU with other tasks running on the system.
How a thread is allocated CPU time is covered in greater depth in "Scheduling
and Priority," later in the chapter.
When certain events happen to a RUNNABLE thread, the thread may enter
the NOT RUNNABLE state. When a thread is NOT RUNNABLE, it is still
alive, but it is not eligible for execution. The thread is not allocated time on
the CPU. Some of the events that may cause a thread to become NOT RUNNABLE
include the following:
- The thread is waiting for an I/O operation to complete
- The thread has been put to sleep for a certain period of time (using the sleep()
method)
- The wait() method has been called (as discussed in "Synchronization,"
later in this chapter)
- The thread has been suspended (using the suspend() method)
A NOT RUNNABLE thread becomes RUNNABLE again when the condition
that caused the thread to become NOT RUNNABLE ends (I/O has completed, the
thread has ended its sleep() period, and so on). During the lifetime of
a thread, the thread may frequently move between the RUNNABLE and NOT
RUNNABLE states.
When a thread terminates, it is said to be DEAD. Threads can become DEAD
in a variety of ways. Usually, a thread dies when its run() method returns.
A thread may also die when its stop() or destroy() method is called.
A thread that is DEAD is permanently DEAD--there is no way to resurrect
a DEAD thread.
-
NOTE: When a thread dies, all the resources
consumed by the thread--including the Thread object itself--become eligible
for reclamation by the garbage collector (if, of course, they are not referenced
elsewhere). Programmers are responsible for cleaning up system resources (closing
open files, disposing of graphics contexts, and so on) while a thread is terminating,
but no cleanup is required after a thread dies.
The Thread API
The following sections present a detailed analysis of the Java Thread
API.
Constructors
The Thread class has seven different constructors:
public Thread();
public Thread(Runnable target);
public Thread(Runnable target, String name);
public Thread(String name);
public Thread(ThreadGroup group, Runnable target);
public Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, String name);
These constructors represent most of the combinations of three different parameters:
thread name, thread group, and a Runnable target object. To understand the
constructors, you must understand the three parameters:
- name is the (string) name to be assigned to the thread. If you fail
to specify a name, the system generates a unique name of the form Thread-N,
where N is a unique integer.
- target is the Runnable instance whose run() method
is executed as the main method of the thread.
- group is the ThreadGroup to which this thread will be added.
(The ThreadGroup class is discussed in detail later in this chapter.)
Constructing a new thread does not begin the execution of that thread. To launch
the Thread object, you must invoke its start() method.
When creating a thread, the priority and daemon status of the new thread are set
to the same values as the thread from which the new thread was created.
-
CAUTION: Although it is possible to allocate
a thread using new Thread(), it is not useful to do so. When constructing
a thread directly (without subclassing), the Thread object requires a target
Runnable object because the Thread class itself does not contain
your application's logic.
Naming
public final String getName();
public final void setName(String name);
Every Java thread has a name. The name can be set during construction or with
the setName() method. If you fail to specify a name during construction,
the system generates a unique name of the form Thread-N, where N
is a unique integer; the name can be changed later using setName().
The name of a thread can be retrieved using the getName() method.
Thread names are important because they provide the programmer with a useful way
to identify particular threads during debugging. You should name a thread in such
a way that you (or others) will find the name helpful in identifying the purpose
or function of the thread during debugging.
Starting and Stopping
To start and stop threads once you have created them, you need the following methods:
public void start();
public final void stop();
public final void stop(Throwable obj);
public void destroy();
To begin a new thread, create a new Thread object and call its start()
method. An exception is thrown if start() is called more than once on the
same thread.
As discussed in "Thread States," earlier in this chapter, there are
two main ways a thread can terminate: The thread can return from its run()
method, ending gracefully. Or the thread can be terminated by the stop()
or destroy() method.
When invoked on a thread, the stop() method causes that thread to terminate
by throwing an exception to the thread (a ThreadDeath exception). Calling
stop() on a thread has the same behavior as executing throw new ThreadDeath()
within the thread, except that stop() can also be called from other threads
(whereas the throw statement affects only the current thread).
To understand why stop() is implemented this way, consider what it means
to stop a running thread. Active threads are part of a running program, and each
runnable thread is in the middle of doing something. It is likely that each thread
is consuming system resources: file descriptors, graphics contexts, monitors (to
be discussed later), and so on. If stopping a thread caused all activity on the thread
to cease immediately, these resources might not be cleaned up properly. The thread
would not have a chance to close its open files or release the monitors it has locked.
If a thread were stopped at the wrong moment, it would be unable to free these resources;
this leads to potential problems for the virtual machine (running out of open file
descriptors, for example).
To provide for clean thread shutdown, the thread to be stopped is given an opportunity
to clean up its resources. A ThreadDeath exception is thrown to the thread,
which percolates up the thread's stack and through the exception handlers that are
currently on the stack (including finally blocks). Monitors are also released
by this stack-unwinding process.
Listing 6.3 shows how calling stop() on a running thread generates a
ThreadDeath exception.
Listing 6.3. Generating a ThreadDeath exception with stop().
class DyingThread extends Thread {
// main(), this class is an application
public static void main(String[] args) {
Thread t = new DyingThread(); // create the thread
t.start(); // start the thread
// wait for a while
try { Thread.sleep(100); } catch (InterruptedException e) { }
t.stop(); // now stop the thread
}
// run(), this class is also a Thread
public void run() {
int n = 0;
PrintStream ps = null;
try {
ps = new PrintStream(new FileOutputStream("big.txt"));
while (true) { // forever
ps.println("n == " + n++);
try { Thread.sleep(5); } catch (InterruptedException e) { }
}
} catch (ThreadDeath td) { // watch for the stop()
System.out.println("Cleaning up.");
ps.close(); // close the open file
// it is very important to rethrow the ThreadDeath
throw td;
} catch (IOException e) {
}
}
}
The DyingThread class has two parts. The main() method spawns
a new DyingThread, waits for a period of time, and then sends a stop()
to the thread. The DyingThread run() method, which is executed
in the spawned thread, opens a file and periodically writes output to that file.
When the thread receives the stop(), it catches the ThreadDeath
exception and closes the open file. It then rethrows the ThreadDeath exception.
When you run the code shown in Listing 6.3, you see the following output:
Cleaning up.
-
NOTE: Java provides a convenient mechanism
for programmers to write "cleanup" code--code that is executed when errors
occur or when a program or thread terminates. (Cleanup involves closing open files,
disposing of graphics contexts, hiding windows, and so on.) Exception handler catch
and finally blocks are good locations for cleanup code.
Programmers use a variety of styles to write cleanup code. Some programmers place
cleanup code in catch(ThreadDeath td) exception handlers (as was done in
Listing 6.3). Others prefer to use catch(Throwable t) exception handlers.
Both these methods are good, but writing cleanup code in a finally block
is the best solution for most situations. A finally block is executed unconditionally,
whether the exception handler exited because of a thrown exception or not. If an
exception was thrown, it is automatically rethrown after the finally block
has completed.
Although the ThreadDeath solution allows the application a high degree
of flexibility, there are problems. By catching the ThreadDeath exception,
a thread can actually prevent stop() from having the desired effect. The
code to do this is trivial:
// prevent stop() from working
catch (ThreadDeath td) {
System.err.println("Just try to stop me. I'm invincible.");
// oh no, I've failed to rethrow td
}
Calling stop() is not sufficient to guarantee that a thread will end.
This is a serious problem for Java-enabled Web browsers; there is no guarantee that
an applet will terminate when stop() is invoked on a thread belonging to
the applet.
The destroy() method is stronger than the stop() method. The
destroy() method is designed to terminate the thread without resorting to
the ThreadDeath mechanism. The destroy() method stops the thread
immediately, without cleanup; any resources held by the thread are not released.
-
CAUTION: The destroy() method
is not implemented in the Java Development Kit, in all versions up to and including
1.1. Calling this method results in a NoSuchMethodError exception. Although
there has been no comment about when this method will be implemented, it is likely
that it will not become available until JavaSoft can implement it in a way that cleans
up the dying thread's environment (locked monitors, pending I/O, and so on).
Scheduling and Priority
Thread scheduling is defined as the mechanism used to determine how RUNNABLE
threads are allocated CPU time (that is, when they actually get to execute for a
period of time on the computer's CPU). In general, scheduling is a complex subject
that uses terms such as pre- emptive, round-robin scheduling, priority-based scheduling,
time-sliced, and so on.
A thread-scheduling mechanism is either preemptive or nonpreemptive. With preemptive
scheduling, the thread scheduler preempts (pauses) a running thread to allow different
threads to execute. A nonpreemptive scheduler never interrupts a running thread;
instead, the nonpreemptive scheduler relies on the running thread to yield control
of the CPU so that other threads can execute. Under nonpreemptive scheduling, other
threads may starve (never get CPU time) if the running thread fails to yield.
Among thread schedulers classified as preemptive, there is a further classification.
A pre- emptive scheduler can be either time-sliced or nontime-sliced. With time-sliced
scheduling, the scheduler allocates a period of time for which each thread can use
the CPU; when that amount of time has elapsed, the scheduler preempts the thread
and switches to a different thread. A nontime-sliced scheduler does not use elapsed
time to determine when to preempt a thread; it uses other criteria such as priority
or I/O status.
Different operating systems and thread packages implement a variety of scheduling
policies. But Java is intended to be platform independent. The correctness of a Java
program should not depend on what platform the program is running on, so the designers
of Java decided to isolate the programmer from most platform dependencies by providing
a single guarantee about thread scheduling: The highest priority RUNNABLE
thread is always selected for execution above lower priority threads. (When multiple
threads have equally high priorities, only one of those threads is guaranteed to
be executing.)
Java threads are guaranteed to be preemptive, but not time sliced. If a higher
priority thread (higher than the current thread) becomes RUNNABLE, the scheduler
preempts the current thread. However, if an equal or lower priority thread becomes
RUNNABLE, there is no guarantee that the new thread will ever be allocated
CPU time until it becomes the highest priority RUNNABLE thread.
NOTE: The current implementation of the
Java VM uses different thread packages on different platforms; thus, the behavior
of the Java thread scheduler varies slightly from platform to platform. It is best
to check with your Java VM supplier to determine whether the VM uses native threads
and whether the platform's native threads are time sliced (some native threading
packages, most notably Solaris threads, are not time sliced).
Even though Java threads are not guaranteed to be time sliced, this should not
be a problem for the majority of Java applications and applets. Java threads release
control of the CPU when they become NOT RUNNABLE. If a thread is waiting
for I/O, is sleeping, or is waiting to enter a monitor, the thread scheduler will
select a different thread for execution. Generally, only threads that perform intensive
numerical analysis (without I/O) will be a problem. A thread would have to be coded
like the following example to prevent other threads from running (and such a thread
would starve other threads only on some platforms--on Windows NT, for example, other
threads would still be allowed to run):
int i = 0;
while (true) {
i++;
}
There are a variety of techniques you can implement to prevent one thread from
consuming too much CPU time:
- Don't write code such as while (true) { }. It is acceptable to have
infinite loops--as long as what takes place inside the loop involves I/O, sleep(),
or interthread coordination (using the wait() and notify() methods,
discussed later in this chapter).
- Occasionally call Thread.yield() when performing operations that are
CPU intensive. The yield() method allows the scheduler to spend time executing
other threads.
- Lower the priority of CPU-intensive threads. Threads with a lower priority run
only when the higher priority threads have nothing to do. For example, the Java garbage
collector thread is a low priority thread. Garbage collection takes place when there
are no higher priority threads that need the CPU; this way, garbage collection does
not needlessly stall the system.
By using these techniques, your applications and applets will be well behaved
on any Java platform.
Setting Thread Priority
public final static int MAX_PRIORITY = 10;
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final int getPriority();
public final void setPriority(int newPriority);
Every thread has a priority. When a thread is created, it inherits the priority
of the thread that created it. The priority can be adjusted subsequently using the
setPriority() method. The priority of a thread can be obtained using getPriority().
There are three symbolic constants defined in the Thread class that represent
the range of priority values: MIN_PRIORITY, NORM_PRIORITY, and
MAX_PRIORITY. The priority values range from 1 to 10,
in increasing priority. An exception is thrown if you attempt to set priority values
outside this range.
Waking Up a Thread
public void interrupt();
public static boolean interrupted();
public boolean isInterrupted();
To send a wake-up message to a thread, call interrupt() on its Thread
object. Calling interrupt() causes an InterruptedException to be
thrown in the thread and sets a flag that can be checked by the running thread using
the interrupted() or isInterrupted() method. Calling Thread.interrupted()
checks the interrupt status of the current thread and resets the interrupt status
to false (in versions before 1.1, the interrupt status is not reset). Calling
isInterrupted() on a Thread object (which can be other than the
current thread) checks the interrupt status of that thread but does not change the
status.
The interrupt() method is useful in waking a thread from a blocking operation
such as I/O, wait(), or an attempt to enter a synchronized method.
-
CAUTION: The interrupt() method
is not fully implemented in the JDK 1.0.x. Calling interrupt() on a thread
sets the interrupted flag but does not throw an InterruptedException
or end a blocking operation in the target thread; threads must check interrupted()
to determine whether the thread has been interrupted.
The interrupt() method is fully implemented in the Java virtual machine,
version 1.1.
Suspending and Resuming Thread Execution
public final void suspend();
public final void resume();
Sometimes, it is necessary to pause a running thread. You can do so using the
suspend() method. Calling the suspend() method ensures that a thread
will not be run. The resume() method reverses the suspend() operation.
A call to suspend() puts the thread in the NOT RUNNABLE state.
However, calling resume() does not guarantee that the target thread will
become RUNNABLE; other events may have caused the thread to be NOT RUNNABLE
(or DEAD).
Putting a Thread to Sleep
public static void sleep(long millisecond);
public static void sleep(long millisecond, int nanosecond);
To pause the current thread for a specified period of time, call one of the varieties
of the sleep() method. For example, Thread.sleep(500) pauses the
current thread for half a second, during which time the thread is in the NOT
RUNNABLE state. When the specified time expires, the current thread again becomes
RUNNABLE.
-
CAUTION: In the JDK versions 1.0.x and
1.1, the sleep(int millisecond, int nanosecond) method
uses the nanosecond parameter to round the millisecond parameter to the nearest millisecond.
Sleeping is not yet supported in nanosecond granularity.
Making a Thread Yield
public static void yield();
The yield() method is used to give a hint to the thread scheduler that now
would be a good time to run other threads. If many threads are RUNNABLE
and waiting to execute, the yield() method is guaranteed to switch to a
different RUNNABLE thread only if the other thread has at least as high
a priority as the current thread.
Waiting for a Thread to End
public final void join();
public final void join(long millisecond);
public final void join(long millisecond, int nanosecond);
Programs sometimes have to wait for a specific thread to terminate; this is referred
to as joining the thread. To wait for a thread to terminate, invoke one of the join()
methods on its Thread object. For example:
Thread t = new OperationINeedDoneThread();
t.start();
.... // do some other stuff
t.join(); // wait for the thread to complete
The two join() methods with time parameters are used to specify a timeout
for the join() operation. If the thread does not terminate within the specified
amount of time, join() returns anyway. To determine whether a timeout has
happened, or whether the thread has ended, use the Thread method isAlive().
join() with no parameters waits forever for the thread to terminate.
-
CAUTION: In the JDK versions 1.0.x and
1.1, the join(int millisecond, int nanosecond) method
uses the nanosecond parameter to round the millisecond parameter to the nearest millisecond.
Joining is not yet supported in nanosecond granularity.
Understanding Daemon Threads
public final boolean isDaemon();
public final void setDaemon(boolean on);
Some threads are intended to be "background" threads, providing service
to other threads. These threads are referred to as daemon threads. When only daemon
threads remain alive, the Java virtual machine process exits.
The Java virtual machine has at least one daemon thread, known as the garbage
collection thread. The garbage collection thread is a low priority thread, executing
only when there is nothing else for the system to do.
The setDaemon() method sets the daemon status of this thread. The isDaemon()
method returns true if this thread is a daemon thread; it returns false
otherwise.
Miscellaneous Thread Methods
The countStackFrames() method returns the number of active stack frames
(method activations) currently on this thread's stack. The thread must be suspended
when this method is invoked. Following is this method's signature:
public int countStackFrames();
The getThreadGroup() method returns the ThreadGroup class to
which this thread belongs. A thread is always a member of a single ThreadGroup
class. Following is this method's signature:
public final ThreadGroup getThreadGroup();
The isAlive() method returns true if start() has been
called on this thread and if this thread has not yet died. In other words, isAlive()
returns true if this thread is RUNNABLE or NOT RUNNABLE
and false if this thread is NEW or DEAD. Following is
this method's signature:
public final boolean isAlive();
The currentThread() method returns the Thread object for the
current sequence of execution. Following is this method's signature:
public static Thread currentThread();
The activeCount() method returns the number of threads in the currently
executing thread's ThreadGroup class. Following is this method's signature:
public static int activeCount();
The enumerate() method returns (through the tarray parameter)
a list of all threads in the current thread's ThreadGroup class. Following
is this method's signature:
public static int enumerate(Thread tarray[]);
The dumpStack() method is used for debugging. It prints a method-by-method
list of the stack trace for the current thread to the System.err output
stream. Following is this method's signature:
public static void dumpStack();
The toString() method returns a debugging string that describes this
thread. Following is this method's prototype:
public String toString();
The ThreadGroup API
Each Java thread belongs to exactly one ThreadGroup instance. The ThreadGroup
class is used to assist with the organization and management of similar groups of
threads. For example, thread groups can be used by Web browsers to group all threads
belonging to a single applet. Single commands can be used to manage the entire group
of threads belonging to the applet.
ThreadGroup objects form a tree-like structure; groups can contain both
threads and other groups. The top thread group is named system; it contains
several system-level threads (such as the garbage collector thread). The system
group also contains the main ThreadGroup object; the main group
contains a main Thread--the thread in which main() is
run. Figure 6.4 is a graphical representation of the ThreadGroup tree.
Constructors
The ThreadGroup class has two constructors. Both constructors require
that you specify a name for the new thread group. One of the constructors takes a
reference to the parent group of the new ThreadGroup; the constructor that
does not take the parent parameter uses the group of the currently executing thread
as the parent of the new group.
public ThreadGroup(String name);
public ThreadGroup(ThreadGroup parent, String name);
Initially, the new ThreadGroup object contains no threads or other thread
groups.
Figure 6.4.
The ThreadGroup tree.
Thread Helper Methods
The ThreadGroup class contains a few methods that operate on the threads
within the group. These methods are "helper" in nature; they invoke the
same-named Thread method on all threads within the group (recursively, to
thread groups within this group).
public final void suspend();
public final void resume();
public final void stop();
public final void destroy();
The helper methods include suspend(), resume(), stop(),
and destroy(). Here is an example of how to stop an entire group of threads
with a single method call:
ThreadGroup group = new ThreadGroup("client threads");
while (some_condition) {
Thread t = new Thread(group);
t.start();
...
}
...
if (kill_em_all) { // stop all of the threads
group.stop();
}
The other thread group helper methods can be called in a similar manner.
Priority
ThreadGroup trees can assist in the management of thread priority. After
calling setMaxPriority() on a ThreadGroup object, no thread within
the group's tree can use setPriority() to set a priority higher than the
specified maximum value. (Priorities of threads already in the group are not affected.)
public final int getMaxPriority();
public final void setMaxPriority(int pri);
The getMaxPriority() method returns the maximum priority value of this
ThreadGroup tree.
ThreadGroup Tree Navigation
Each thread group can contain both threads and thread groups. The activeCount()
and activeCountGroup() methods return the number of contained threads and
groups, respectively. Following are the method signatures:
public int activeCount();
public int activeGroupCount();
The activeCount() method returns the number of threads that are members
of this ThreadGroup tree (recursively).
The activeCountGroup() method returns the number of ThreadGroups
that are members of this ThreadGroup tree (recursively).
The following enumerate() methods can be used to retrieve the list of
threads or groups in this ThreadGroup object:
public int enumerate(Thread list[]);
public int enumerate(Thread list[], boolean recurse);
public int enumerate(ThreadGroup list[]);
public int enumerate(ThreadGroup list[], boolean recurse);
The recurse parameter, if true, causes the retrieval of all
the threads or groups within this ThreadGroup tree (recursively). If recurse
is false, only the threads or groups in this immediate ThreadGroup
object are retrieved. The enumerate() methods lacking the recurse
parameter perform in the same way as the enumerate() methods with recurse
set to true.
The parentOf() method returns true if this thread group is the
parent of the specified group; it returns false otherwise. Following is
this method's syntax:
public final boolean parentOf(ThreadGroup g);
The getParent() method returns the parent of this thread group, or null
if this ThreadGroup is the top-level ThreadGroup. Following is
this method's syntax:
public final ThreadGroup getParent();
The list() method prints debugging information about this ThreadGroup's
tree (threads and groups) to System.out. Following is this method's syntax:
public void list();
Miscellaneous ThreadGroup Methods
The getName() method returns the name of this thread group. Following
is this method's syntax:
public final String getName();
Some thread groups, like some threads, can be referred to as daemons. When a ThreadGroup
object is a daemon group (setDaemon(true) has been called), the group is
destroyed once all its threads and groups have been removed.
public final boolean isDaemon();
public final void setDaemon(boolean daemon);
The isDaemon() method returns true if this thread group is a
daemon; it returns false otherwise.
The toString() method returns debugging information about this thread
group. Following is this method's syntax:
public String toString();
When a thread exits because it failed to catch an exception, the uncaughtException()
method of the thread's group is invoked with the Thread object and the exception
(Throwable) as parameters:
public void uncaughtException(Thread t, Throwable
e);
The default behavior of uncaughtException() is to pass the thread and
exception to the parent of this thread group. The system thread group, if
reached, calls the Throwable exception's printStackTrace() method,
dumping the stack trace of the exception to System.err.
Security Features
Threads and thread groups are considered critical system resources that must be
protected by Java's security features. The precise implementation of the security
policy depends on the environment. When running a Java application, there is no security
unless you install a SecurityManager using System.setSecurityManager().
Applets, however, use the SecurityManager installed by the browser environment.
When you run an applet under Netscape Navigator 3.0, for example, the applet is allowed
to modify only the threads and thread groups created by the current applet; attempts
to modify other threads or groups result in a SecurityException.
The Thread class has security (as implemented by the current SecurityManager
object) implemented for the following methods:
- Thread(ThreadGroup group)
- Thread(ThreadGroupgroup, Runnable target, String name)
- Thread(ThreadGroupgroup, String name)
- stop()
- suspend( and resume()
setPriority)
setName)
setDaemon)
The ThreadGroup class has security (as implemented by the current SecurityManager
object) implemented for the following methods:
- ThreadGroup(ThreadGroup parent, String name)
- setDaemon()
- setMaxPriority()
- stop()
- suspend( and resume()
- destroy()
Concurrency
One of the most powerful features of the Java programming language is that it
can run multiple threads of control. Performing multiple tasks at the same time seems
natural from the user's perspective--for example, simultaneously downloading a file
from the Internet, performing a spreadsheet recalculation, and printing a document.
From a programmer's point of view, however, managing concurrency is not as natural
as it seems. Concurrency requires the programmer to take special precautions to ensure
that Java objects are accessed in a thread-safe manner.
There is nothing obvious about threads that makes threaded programs unsafe; nevertheless,
threaded programs can be subject to hazardous situations unless you take appropriate
measures to make them safe.
The following example demonstrates how a threaded program can be unsafe:
public class Counter {
private int count = 0;
public int incr() {
int n = count;
count = n + 1;
return n;
}
}
As Java classes go, the Counter class is simple, having only one attribute
and one method. As its name implies, the Counter class is used to count
things, such as the number of times a button is pressed or the number of times the
user visits a particular Web site. The incr() method is the heart of the
class, returning and incrementing the current value of the counter. However, the
incr() method has a problem; it is a source of unpredictable behavior in
a multithreaded environment.
Consider a situation in which a Java program has two runnable threads, both of
which are about to execute this line of code (affecting the same Counter
object):
int cnt = counter.incr();
The programmer cannot predict or control the order in which these two threads
are run. The Java thread scheduler has full authority over thread scheduling. There
are no guarantees about which thread will receive CPU time, when the threads will
execute, or how long each thread will be allowed to execute. Either thread may be
interrupted by the scheduler at any time (remember that Java's thread scheduler is
preemptive). On a multiprocessor machine, both threads may execute concurrently on
separate processors.
Table 6.1 describes one possible sequence of execution of the two threads. In
this scenario, the first thread is allowed to run until it completes its call to
counter.incr(); then the second thread does the same. There are no surprises
in this scenario. The first thread increments the Counter value to 1,
and the second thread increments the value to 2.
Table 6.1. Counter scenario I.
Thread 1 |
Thread 2 |
Count |
cnt = counter.incr(); |
--- |
0 |
n = count; // 0 |
--- |
0 |
count = n + 1; // 1 |
--- |
1 |
return n; // 0 |
--- |
1 |
--- |
cnt = counter.incr(); |
1 |
--- |
n = count; // 1 |
1 |
--- |
count = n + 1; // 2 |
2 |
--- |
return n; // 1 |
2 |
Table 6.2 describes a somewhat different sequence of execution. In this scenario,
the first thread is interrupted by a context switch (a switch to a different thread)
during execution of the incr() method. The first thread remains temporarily
suspended, and the second thread is allowed to proceed. The second thread executes
its call to the incr() method, incrementing the Counter value to
1. When the first thread resumes, a problem becomes evident. The Counter's
value is not updated to the value 2, as you would expect, but is instead
set again to the value 1.
Table 6.2. Counter scenario II.
Thread 1 |
Thread 2 |
Count |
cnt = counter.incr(); |
--- |
0 |
n = count; // 0 |
--- |
0 |
--- |
cnt = counter.incr(); |
0 |
--- |
n = count; // 0 |
0 |
--- |
count = n + 1; // 1 |
1 |
--- |
return n; // 0 |
1 |
count = n + 1; // 1 |
--- |
1 |
return n; // 0 |
--- |
1 |
By examining Thread 1 in Table 6.2, you can see a problematic sequence of
operations. After entering the incr() method, the value of the count
attribute (0) is stored in a local variable, n. The thread is then
suspended for a period of time while a different thread executes. (It is important
to note that the count attribute is modified by the second thread during
this time.) When Thread 1 resumes, it stores the value n + 1 (1)
back in the count attribute. Unfortunately, this is no longer a correct
value for the counter because the counter was already incremented to 1 by
Thread 2.
The problem outlined by Table 6.2 is called a race condition--the outcome of the
program is affected by the order in which the program's threads are allocated CPU
time. It is usually considered inappropriate to allow race conditions to affect a
program's result. Consider a medical device that monitors a patient's blood pressure.
If this device were affected by race conditions in its software, it might report
an incorrect reading to the physician. The physician would base medical treatment
decisions on incorrect information--a bad situation for the patient, doctor, insurance
company, and software vendor!
All multithreaded programs, even Java programs, can suffer from race conditions.
Fortunately, Java provides the programmer with the necessary tools to manage concurrency--monitors.
Monitors
Many texts on computer science and operating systems deal with the issue of concurrent
programming. Concurrency has been the subject of much research over the years, and
many concurrency-control solutions have been proposed and implemented. These solutions
include the following:
- Critical sections
- Semaphores
- Mutexes
- Database record locking
- Monitors
Java implements a variant of the monitor approach to concurrency.
The concept of a monitor was introduced by C. A. R. Hoare in a 1974 paper published
in the Communications of the ACM. Hoare described a special-purpose object, called
a monitor, which applies the principle of mutual exclusion to groups of procedures
(mutual exclusion is a fancy way of saying "one thread at a time"). In
Hoare's model, each group of procedures requiring mutual exclusion is placed under
the control of a single monitor. At run time, the monitor allows only one thread
at a time to execute a procedure controlled by the monitor. If another thread tries
to invoke a procedure controlled by the monitor, that thread is suspended until the
first thread completes its call.
Java monitors remain true to Hoare's original concept, with a few minor variations
(which are not discussed here). Monitors in Java enforce mutually exclusive access
to methods; more specifically, Java monitors enforce mutually exclusive access to
synchronized methods. (The synchronized keyword is an optional
method modifier. If the synchronized keyword appears before the return type
and signature of the method, the method is referred to as a "synchronized
method.")
Every Java object has an associated monitor. synchronized methods that
are invoked on an object use that object's monitor to limit concurrent access to
that object. When a synchronized method is invoked on an object, the object's
monitor is consulted to determine whether any other thread is currently executing
a synchronized method on the object. If no other thread is executing a synchronized
method on that object, the current thread is allowed to enter the monitor. (Entering
a monitor is also referred to as locking the monitor, or acquiring ownership of the
monitor.) If a different thread has already entered the monitor, the current thread
must wait until the other thread leaves the monitor.
Metaphorically, a Java monitor acts as an object's gatekeeper. When a synchronized
method is called, the gatekeeper allows the calling thread to pass and then closes
the gate. While the thread is still in the synchronized method, subsequent
synchronized method calls to that object from other threads are blocked.
Those threads line up outside the gate, waiting for the first thread to leave. When
the first thread exits the synchronized method, the gatekeeper opens the
gate, allowing a single waiting thread to proceed with its synchronized
method call. The process repeats.
In plain English, a Java monitor enforces a one-at-a-time approach to concurrency.
This is also known as serialization (not to be confused with "object serialization,"
which is the Java library for reading and writing objects on a stream).
-
NOTE: Programmers already familiar with
multithreaded programming in a different programming language often confuse monitors
with critical sections. Java monitors are not like traditional critical sections.
Declaring a method synchronized does not imply that only one thread at a
time can execute that method, as is the case with a critical section. Monitors imply
that only one thread can invoke that method (or any synchronized method)
on a particular object at any given time. Java monitors are associated with objects,
not with blocks of code. Two threads can concurrently execute the same synchronized
method, provided that the method is invoked on different objects (that is, a.method()
and b.method(), where a != b).
To demonstrate how monitors operate, let's rewrite the Counter example
from the preceding section to take advantage of monitors, using the synchronized
keyword:
public class Counter2 {
private int count = 0;
public synchronized int incr() {
int n = count;
count = n + 1;
return n;
}
}
Note that the incr() method has not been modified except for the addition
of the synchronized keyword.
What would happen if this new Counter2 class were used in the scenario
presented in Table 6.2 (the race condition)? The outcome of the same sequence of
context switches is listed in Table 6.3.
Table 6.3. Counter scenario II, revised.
Thread 1 |
Thread 2 |
Count |
cnt = counter.incr(); |
--- |
0 |
(acquires the monitor) |
--- |
0 |
n = count; // 0 |
--- |
0 |
--- |
cnt = counter.incr(); |
0 |
--- |
(can't acquire monitor) |
0 |
count = n + 1; // 1 |
(blocked) |
1 |
return n; // 0 |
(blocked) |
1 |
(releases the monitor) |
(blocked) |
1 |
--- |
(acquires the monitor) |
1 |
--- |
n = count; // 1 |
1 |
--- |
count = n + 1; // 2 |
2 |
--- |
return n; // 1 |
2 |
--- |
(releases the monitor) |
2 |
In Table 6.3, the sequence of operations begins the same as the scenario in
Table 6.2. Thread 1 starts executing the incr() method of the Counter2
object but is interrupted by a context switch. In this example, however, when Thread
2 attempts to execute the incr() method on the same Counter2 object,
the thread can't acquire the monitor and is blocked; the monitor is already owned
by Thread 1. Thread 2 is suspended until the monitor becomes available. When Thread
1 releases the monitor, Thread 2 can then acquire the monitor and continue running.
The synchronized keyword is Java's solution to the concurrency control
problem. As you saw in the Counter example, the potential race condition
was eliminated by adding the synchronized modifier to the incr()
method. All accesses to the incr() method of a counter were serialized by
the addition of the synchronized keyword. Generally speaking, any method
that modifies an object's attributes should be synchronized with the synchronized
keyword.
-
NOTE: You may be wondering when you will
see an actual monitor object. Anecdotal information has been presented about monitors,
but you probably want to see some official documentation about what a monitor is
and how you access it. Unfortunately, that is not possible. Java monitors have no
official standing in the language specification, and their implementation is not
directly visible to the programmer. Monitors are not Java objects--they have no attributes
or methods. Monitors are a concept beneath Java's implementation of multithreading
and concurrency. It is possible to access Java monitors at the native code level
in the 1.x release of the Java virtual machine from Sun--the 1.1 Java Native Interface
specification defines two methods that operate on monitors: MonitorEnter()
and MonitorExit().
Non-synchronized Methods
Java monitors are used only in conjunction with synchronized methods.
Methods that are not declared synchronized do not attempt to acquire ownership
of an object's monitor before executing--they ignore monitors entirely. At any given
moment, at most one thread can execute a synchronized method on an object,
but an arbitrary number of threads can execute non-synchronized methods.
This can lead to some surprising situations if you are not careful in deciding which
methods should be synchronized. Consider the Account class in Listing 6.4.
Listing 6.4. The Account class.
class Account {
private int balance;
public Account(int balance) {
this.balance = balance;
}
public synchronized void transfer(int amount, Account destination) {
synchronized (destination) {
this.withdraw(amount);
Thread.yield(); // force a context switch
destination.deposit(amount);
}
}
public synchronized void withdraw(int amount) {
if (amount > balance) {
throw new RuntimeException("No overdraft protection!");
}
balance -= amount;
}
public synchronized void deposit(int amount) {
balance += amount;
}
public int getBalance() {
return balance;
}
}
The attribute-modifying methods of the Account class are declared synchronized,
but the getBalance() method is not synchronized. It appears that this class
has no problem with race conditions--but it does!
To understand the race condition to which the Account class is subject,
consider how a bank deals with accounts. To a bank, the correctness of its accounts
is of the utmost importance--a bank that makes accounting errors or reports incorrect
information would not have happy customers. To avoid reporting incorrect information,
a bank would likely disable "inquiries" on an account while a transaction
involving the account is in progress. This prevents the customer from viewing the
result of a partially complete transaction. In Listing 6.4, the Account
class's getBalance() method is not synchronized, and this can lead
to problems.
Consider two Account objects and two different threads performing actions
on these accounts. One thread is performing a balance transfer from one account to
the other. The second thread is performing a balance inquiry. This code demonstrates
the suggested activity:
public class XferTest implements Runnable {
public static void main(String[] args) {
XferTest xfer = new XferTest();
xfer.a = new Account(100);
xfer.b = new Account(100);
xfer.amount = 50;
Thread t = new Thread(xfer);
t.start();
Thread.yield(); // force a context switch
System.out.println("Inquiry: Account a has : $" + xfer.a.getBalance());
System.out.println("Inquiry: Account b has : $" + xfer.b.getBalance());
}
public Account a = null;
public Account b = null;
public int amount = 0;
public void run() {
System.out.println("Before xfer: a has : $" + a.getBalance());
System.out.println("Before xfer: b has : $" + b.getBalance());
a.transfer(amount, b);
System.out.println("After xfer: a has : $" + a.getBalance());
System.out.println("After xfer: b has : $" + b.getBalance());
}
}
In this example, two Accounts are created, each with a $100 balance. A transfer
is then initiated to move $50 from one account to the other. The transfer is not
an operation that should affect the total balance of the two accounts; that is, the
sum of the balance of the two accounts should remain constant at $200. If the balance
inquiry is performed at just the right time, however, it is possible that the total
amount of funds in these accounts could be incorrectly reported. If this program
is run using the JDK version 1.0 for Solaris, the following output is printed:
Before xfer: a has : $100
Before xfer: b has : $100
Inquiry: Account a has : $50
Inquiry: Account b has : $100
After xfer: a has : $50
After xfer: b has : $150
The inquiry reports that the first account contains $50 and the second account
contains $100. That's not $200! What happened to the other $50? Nothing has "happened"
to the money--except that it is in the process of being transferred to the second
account when the balance inquiry scans the accounts. Because the getBalance()
method is not synchronized, a customer would have no problem executing an inquiry
on accounts involved in the balance transfer. The lack of synchronization can leave
some customer wondering why the accounts are $50 short.
If the getBalance() method is declared synchronized, the application
has a different result. The modified code follows:
public synchronized int getBalance() {
return balance;
}
The balance inquiry is blocked until the balance transfer is complete. Here is
the modified program's output:
Before xfer: a has : $100
Before xfer: b has : $100
Inquiry: Account a has : $50
Inquiry: Account b has : $150
After xfer: a has : $50
After xfer: b has : $150
Advanced Monitor Concepts
Monitors sound pretty simple. Add the synchronized modifier to your methods,
and that's all there is to it. Well, not quite. Monitors themselves may be simple,
but taken together with the rest of the programming environment, there are a few
issues you should understand before you use synchronized methods. The following
sections present a few tips and techniques you should master to become expert in
concurrent Java programming.
static synchronized Methods
Methods that are declared synchronized attempt to acquire ownership of
the target object's monitor. But what about static methods (methods that
do not have an associated object)?
The language specification is fairly clear, if brief, about static synchronized
methods. When a static synchronized method is called, the monitor acquired
is said to be a per-class monitor--that is, there is one monitor for each class that
regulates access to all static methods of that class. Only one static
synchronized method in a class can be active at a given moment.
The synchronized Statement
It is not possible to use synchronized methods on some types of objects.
For example, it is not possible to add any methods to Java array objects (much less
synchronized methods). To get around this restriction, Java has a second
way of interacting with an object's monitor. The synchronized statement
is defined to have the following syntax:
synchronized ( Expression ) Statement
Executing a synchronized statement has the same effect as calling a synchronized
method--ownership of an object's monitor is acquired before a block of code can be
executed. With the synchronized statement, the object whose monitor is up
for grabs is the object resulting from Expression (which must be an object type,
not an elemental type like int, double, and so on).
One of the most important uses of the synchronized statement involves
controlling access to array objects. The following example demonstrates how to use
the synchronized statement to provide thread-safe access to an array:
void safe_lshift(byte[] array, int count) {
synchronized(array) {
System.arraycopy(array, count, array, 0, array.size - count);
}
}
Before modifying the array in this example, the virtual machine assigns ownership
of array's monitor to the currently executing thread. Other threads trying
to acquire array's monitor are forced to wait until the array-copy operation
is complete. Of course, accesses to the array that are not guarded by a synchronized
statement are not blocked; so be careful!
The synchronized statement is also useful when modifying an object's
public variables directly. Here's an example:
void call_method(SomeClass obj) {
synchronized(obj) {
obj.variable = 5;
}
}
-
PUBLIC OR NOT?
There is debate within the Java community about the potential danger of declaring
attributes to be public. When concurrency is considered, it becomes apparent
that public attributes can lead to thread-unsafe code. Here's why: public
attributes can be accessed by any thread without the benefit of protection by a synchronized
method. When you declare an attribute public, you relinquish control over
updates to that attribute; any programmer using your code has a license to access
(and update) public attributes directly.
In general, it is not a good idea to declare non-final attributes to
be public. Not only can doing so introduce thread-safety problems, it can
make your code difficult to modify and support in later revisions.
Note, however, that Java programmers frequently define immutable symbolic constants
as public final class attributes (such as Event.ACTION_EVENT).
Attributes declared this way do not have thread-safety issues. (Race conditions involve
only objects whose values can be modified.)
When Not to Be synchronized
By now, you should be able to write thread-safe code using the synchronized
keyword. When should you really use the synchronized keyword? Are there
situations in which you should not use synchronized? Are there drawbacks
to using synchronized?
The most common reason developers don't use synchronized is that they
write single-threaded, single-purpose code. For example, CPU-bound tasks do not benefit
much from multithreading. A compiler does not perform much better if it is threaded.
The Java compiler from Sun does not contain many synchronized methods. For
the most part, it assumes that it is executing in its own thread of control, without
having to share its resources with other threads.
Another common reason for avoiding synchronized methods is that they
do not perform as well as non-synchronized methods. In simple tests in the
JDK version 1.0.1 from Sun, synchronized methods have been shown to be three
to four times slower than their non- synchronized counterparts. Although
this doesn't mean your entire application will be three or four times slower, it
is a performance issue nonetheless. Some programs demand that every ounce of performance
be squeezed out of the runtime system. In this situation, it may be appropriate to
avoid the performance overhead associated with synchronized methods.
Java 1.1 Inner Classes
With the release of Java 1.1, Java now supports inner classes (refer to Chapter
5, "Classes, Packages, and Interfaces," for an introduction to inner classes).
Inner classes are classes declared within another class. An instance of an inner
class has a special relationship to an instance of the outer class--referred to as
the enclosing instance. Inner-class instances are permitted to access all the fields
of the enclosing instance, including private fields. Additionally, an inner-class
instance can modify the variables of its enclosing instance.
Inner classes introduce potential problems for programs that use threads. Consider
the following example of an inner class and an enclosing class:
public class Enclosing {
private int privateVar = 1;
public synchronized void update() {
privateVar = privateVar + 1;
}
public class Inner {
public synchronized void update() {
privateVar = privateVar + 1;
}
}
}
The Enclosing class has three fields: a private variable, a
synchronized method, and the Inner class. This example demonstrates
that the private variables of an enclosing instance can be updated from
methods of the enclosing instance and the inner instance--both Enclosing
and Inner have an update() method that modifies privateVar.
Calling a synchronized method from the enclosing instance is a safe way
to update the enclosing instance:
Enclosing enclosingInstance = getEnclosingInstance();
enclosingInstance.update(); // SAFE
However, it is not particularly safe to call a synchronized method from
an inner instance to update the enclosing instance:
Enclosing enclosingInstance = getEnclosingInstance();
Enclosing.Inner innerInstance = enclosingInstance.new Inner();
innerInstance.update(); // UNSAFE
This second code example is unsafe because the use of inner classes involves more
than one object--and thus, more than one monitor. When the code innerInstance.update()
is called, the only monitor acquired is that of the innerInstance. The monitor
of the enclosingInstance is never acquired, even though its privateVar
is updated. To make the code thread safe, the Inner class's update()
method should acquire the monitor of its enclosing instance, like this:
class Inner {
public synchronized void update() {
synchronized (Enclosing.this) { // must also lock the enclosing instance
privateVar = privateVar + 1;
}
}
}
In general, inner classes that access variables from an enclosing instance should
use the synchronized statement to lock the enclosing instance while its
variables are being accessed. The generalized form for this type of lock follows:
synchronized (name-of-enclosing-class . this) {
// your code here
}
Deadlocks
Sometimes referred to as a deadly embrace, a deadlock is one of the worst situations
that can happen in a multithreaded environment. Java programs are not immune to deadlocks,
and programmers must take care to avoid them.
A deadlock is a situation that causes two or more threads to hang, that is, to
be unable to proceed. In the simplest case, two threads are each trying to acquire
a monitor that is already owned by the other thread. Each thread goes to sleep, waiting
for the desired monitor to become available--but the monitors never become available.
(The first thread waits for the monitor owned by the second thread, and the second
thread waits for the monitor owned by the first thread. Because each thread is waiting
for the other, it never releases its monitor to the other thread.)
The sample application in Listing 6.5 should give you an understanding of how
a deadlock happens.
Listing 6.5. A deadlock.
public class Deadlock implements Runnable {
public static void main(String[] args) {
Deadlock d1 = new Deadlock();
Deadlock d2 = new Deadlock();
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
d1.grabIt = d2;
d2.grabIt = d1;
t1.start();
t2.start();
try { t1.join(); t2.join(); } catch(InterruptedException e) { }
System.exit(0);
}
Deadlock grabIt;
public synchronized void run() {
try { Thread.sleep(2000); } catch(InterruptedException e) { }
grabIt.sync_method();
}
public synchronized void sync_method() {
try { Thread.sleep(2000); } catch(InterruptedException e) { }
System.out.println("in sync_method");
}
}
In this class, main() launches two threads, each of which invokes the
synchronized run() method on a Deadlock object. When the first
thread wakes up, it attempts to call the sync_method() of the other Deadlock
object. Obviously, the second Deadlock's monitor is owned by the second
thread, so the first thread begins waiting for the monitor. When the second thread
wakes up, it tries to call the sync_method() of the first Deadlock
object. Because that Deadlock's monitor is already owned by the first thread,
the second thread begins waiting. Because the threads are waiting for each other,
neither will ever wake up.
NOTE: If you run the deadlock application
shown in Listing 6.5, you will notice that it never exits. That is understandable;
after all, that is what a deadlock is. How can you tell what is really going on inside
the virtual machine? There is a trick you can use with the Solaris/UNIX JDK to display
the status of all threads and monitors: Press Ctrl+\ in the terminal window in which
the Java application is running. This action sends the virtual machine a signal to
dump the state of the VM. Here is a partial listing of the monitor table dumped several
seconds after launching the deadlock application:
Deadlock@EE300840/EE334C20 (key=0xee300840): monitor owner: "Thread-5"
Waiting to enter:
"Thread-4"
Deadlock@EE300838/EE334C18 (key=0xee300838): monitor owner: "Thread-4"
Waiting to enter:
"Thread-5"
Numerous algorithms are available for preventing and detecting deadlock situations,
but those algorithms are beyond the scope of this chapter (many database and operating
system texts cover deadlock-detection algorithms in detail). Unfortunately, the Java
virtual machine itself does not perform any deadlock detection or notification. There
is nothing that prevents the virtual machine from doing so, however, so this behavior
may be added to future versions of the virtual machine.
Using volatile
It is worth mentioning that the volatile keyword is supported as a variable
modifier in Java. The language specification states that the volatile qualifier
instructs the compiler to generate loads and stores on each access to an attribute,
rather than caching the value in a register. The intent of the volatile
keyword is to provide thread-safe access to an attribute, but volatile falls
short of this goal.
In the 1.x JDK virtual machine, the volatile keyword is ignored. It is
unclear whether volatile has been abandoned in favor of monitors and synchronized
methods or whether the keyword was included solely for a C and C++ look and feel.
Regardless, volatile is useless--use synchronized methods rather
than the volatile keyword.
Synchronization
After learning how synchronized methods are used to make Java programs
thread safe, you may wonder what the big deal is about monitors. They are just object
locks, right? Not true! Monitors are more than locks; monitors are also used to coordinate
multiple threads by using the wait() and notify() methods available
in every Java object.
The Need for Thread Coordination
In a Java program, threads are often interdependent--one thread can depend on
another thread to complete an operation or to service a request. For example, a spreadsheet
program can run an extensive recalculation as a separate thread. If a user-interface
(UI) thread attempts to update the spreadsheet's display, the UI thread should coordinate
with the recalculation thread, starting the screen update only when the recalculation
thread has successfully completed.
There are many other situations in which it is useful to coordinate two or more
threads. The following list identifies only some of the possibilities:
- Shared buffers are often used to communicate data between threads. In
this scenario, one thread writes to a shared buffer (the writer) and one thread reads
from the buffer (the reader). When the reader thread attempts to read from the buffer,
it should coordinate with the writer thread, retrieving data from the shared buffer
only after the writer thread has put it there. If the buffer is empty, the reader
thread should wait for the data. The writer thread notifies the reader thread when
it has completed filling the buffer so that the reader can continue.
- Many threads may have to perform an identical action, such as loading an image
file across the network. These threads can reduce the overall system load if
only one thread performs the work while the other threads wait for the work to be
completed. (The waiting threads must wait without consuming CPU time by temporarily
transitioning into the NOT RUNNABLE thread state; this is possible, and
is discussed later in this chapter.) This is precisely the model used in the java.awt.MediaTracker
class.
It is no accident that the previous examples repeatedly use the words wait and
notify. These words express the two concepts central to thread coordination: A thread
waits for some condition or event to occur, and you notify a waiting thread that
a condition or event has occurred. The words wait and notify are also used in Java
as the names of the methods you call to coordinate threads: wait() and notify(),
in class Object.
As noted in "Monitors," earlier in this chapter, every Java object has
an associated monitor. That fact turns out to be useful at this point because monitors
are also used to implement Java's thread-coordination primitives. Although monitors
are not directly visible to the programmer, an API is provided in class Object
that enables you to interact with an object's monitor. This API consists of two methods:
wait() and notify().
Conditions, wait(), and notify()
Threads are usually coordinated using a concept known as a condition, or a condition
variable. A condition is a logical statement that must hold true in order for a thread
to proceed; if the condition does not hold true, the thread must wait for the condition
to become true before continuing. In Java, this pattern is usually expressed as follows:
while ( ! the_condition_I_am_waiting_for ) {
wait();
}
First, check to see whether the desired condition is already true. If it is true,
there is no need to wait. If the condition is not yet true, call the wait()
method. When wait() ends, recheck the condition to make sure that it is
now true.
Invoking wait() on an object pauses the current thread and adds the thread
to the condition variable wait queue of the object's monitor. This queue contains
a list of all the threads that are currently blocked inside wait() on that
object. The thread is not removed from the wait queue until notify() is
invoked on that object from a different thread. A call to notify() wakes
a single waiting thread, notifying the thread that a condition of the object has
changed.
There are two additional varieties of the wait() method. The first version
takes a single parameter--a timeout value in milliseconds. The second version has
two parameters--a more precise timeout value, specified in milliseconds and nanoseconds.
These methods are used when you do not want to wait indefinitely for an event. If
you want to abandon the wait after a fixed period of time (referred to as timing
out), you should use either of the following methods:
- wait(long milliseconds);
- wait(long milliseconds, int nanoseconds);
Unfortunately, these methods do not provide a way to determine how the wait()
was ended--whether a notify() occurred or whether the method timed out.
This is not a big problem, however, because you can recheck the wait condition and
the system time to determine which event has occurred.
-
CAUTION: In the JDK versions 1.0.x and
1.1, the wait(int millisecond, int nanosecond) method
uses the nanosecond parameter to round the millisecond parameter to the nearest millisecond.
Waiting is not yet supported in nanosecond granularity.
The wait() and notify() methods must be invoked from within
a synchronized method or from within a synchronized statement.
This requirement is discussed in further detail in "Monitor Ownership,"
later in this chapter.
A Thread Coordination Example
A classic example of thread coordination used in many computer science texts is
the bounded buffer problem. This problem involves using a fixed-size memory buffer
to communicate between two processes or threads. To solve this problem, you must
coordinate the reader and writer threads so that the following are true:
- When the writer thread attempts to write to a full buffer, the writer is suspended
until some items are removed from the buffer.
- When the reader thread removes items from the full buffer, the writer thread
is notified of the buffer's changed condition and may continue writing.
- When the reader thread attempts to read from an empty buffer, the reader is suspended
until some items are added to the buffer.
- When the writer adds items to the empty buffer, the reader thread is notified
of the buffer's changed condition and may continue reading.
The following class listings demonstrate a Java implementation of the bounded
buffer problem. There are three main classes in this example: the Producer,
the Consumer, and the Buffer. Let's start with the Producer:
public class Producer implements Runnable {
private Buffer buffer;
public Producer(Buffer b) {
buffer = b;
}
public void run() {
for (int i=0; i<250; i++) {
buffer.put((char)('A' + (i%26))); // write to the buffer
}
}
}
The Producer class implements the Runnable interface (which
should give you a hint that it will be used in a Thread). When the Producer's
run() method is invoked, 250 characters are written in rapid succession
to a buffer.
The Consumer class is as simple as the Producer:
public class Consumer implements Runnable {
private Buffer buffer;
public Consumer(Buffer b) {
buffer = b;
}
public void run() {
for (int i=0; i<250; i++) {
System.out.println(buffer.get()); // read from the buffer
}
}
}
The Consumer is also a Runnable interface. Its run()
method greedily reads 250 characters from a buffer.
The Buffer class has been mentioned already, including two of its methods:
put(char) and get(). Listing 6.6 shows the Buffer class
in its entirety.
Listing 6.6. The Buffer class.
public class Buffer {
private char[] buf; // buffer storage
private int last; // last occupied position
public Buffer(int sz) {
buf = new char[sz];
last = 0;
}
public boolean isFull() { return (last == buf.length); }
public boolean isEmpty() { return (last == 0); }
public synchronized void put(char c) {
while(isFull()) { // wait for room to put stuff
try { wait(); } catch(InterruptedException e) { }
}
buf[last++] = c;
notify();
}
public synchronized char get() {
while(isEmpty()) { // wait for stuff to read
try { wait(); } catch(InterruptedException e) { }
}
char c = buf[0];
System.arraycopy(buf, 1, buf, 0, --last);
notify();
return c;
}
}
-
NOTE: When you first begin using wait()
and notify(), you may notice a contradiction. The wait() and notify()
methods must be called from synchronized methods, so if wait()
is called inside a synchronized method, how can a different thread enter
a synchronized method in order to call notify()? Doesn't the waiting
thread own the object's monitor, preventing other threads from entering the synchronized
method?
The answer to this paradox is that wait() temporarily releases ownership
of the object's monitor; before wait() can return, however, it must reacquire
ownership of the monitor. By releasing the monitor, the wait() method allows
other threads to acquire the monitor (which gives them the ability to call notify()).
The Buffer class is just that--a storage buffer. You can use put()
to put items into the buffer (in this case, characters), and you can use get()
to get items out of the buffer.
Note the use of wait() and notify() in these methods. In the
put() method, a wait() is performed while the buffer is full; no
more items can be added to the buffer while it is full. At the end of the get()
method, the call to notify() ensures that any thread waiting in the put()
method will be activated and allowed to continue adding an item to the buffer. Similarly,
a wait() is performed in the get() method if the buffer is empty;
no items can be removed from an empty buffer. The put() method calls notify()
to ensure that any thread waiting in get() will be wakened.
-
NOTE: Java provides two classes similar
to the Buffer class presented in this example. These classes, java.io.PipedOutputStream
and java.io.PipedInputStream, are useful in communicating streams of data
between threads. If you unpack the src.zip file shipped with the JDK, you
can examine these classes to see how they handle interthread coordi-nation.
Advanced Thread Coordination
The wait() and notify() methods simplify the task of coordinating
multiple threads in a concurrent Java program. However, to make full use of these
methods, you should understand a few additional details. The following sections present
more material about thread coordination in Java.
Monitor Ownership
The wait() and notify() methods have one major restriction you
must observe: You can call these methods only when the current thread owns the monitor
of the object. Most frequently, wait() and notify() are invoked
from within a synchronized method, as in the following example:
public synchronized void method() {
...
while (!condition) {
wait();
}
...
}
In this case, the synchronized modifier guarantees that the thread invoking
the wait() call already owns the monitor when it calls wait().
If you attempt to call wait() or notify() without first acquiring
ownership of the object's monitor (for example, from a non-synchronized
method), the virtual machine throws an IllegalMonitorStateException. The
following example demonstrates what happens when you call wait() without
first acquiring ownership of the monitor:
public class NonOwnerTest {
public static void main(String[] args) {
NonOwnerTest not = new NonOwnerTest();
not.method();
}
public void method() {
try { wait(); } catch(InterruptedException e) { } // a bad thing to do!
}
}
If you run this Java application, the following text is printed to the terminal:
java.lang.IllegalMonitorStateException: current thread not owner
at java.lang.Object.wait(Object.java)
at NonOwnerTest.method(NonOwnerTest.java:10)
at NonOwnerTest.main(NonOwnerTest.java:5)
When you invoke the wait() method on an object, you must own the object's
monitor if you are to avoid this exception.
-
MONITORS AND THE synchronized STATEMENT:
All Java objects can participate in thread synchronization by using the wait()
and notify() methods. However, the "monitor ownership" requirement
introduces a quirk for some object types, such as arrays. (Strangely enough, Java
array types inherit from the java.lang.Object class, where the wait()
and notify() methods are defined.) The wait() and notify()
methods can be called on Java array objects, but monitor ownership must be established
using the synchronized statement rather than a synchronized method. The
following code demonstrates monitor usage as applied to a Java array:
// wait for an event on this array
Object[] array = getArray();
synchronized (array) {
array.wait();
}
...
// notify waiting threads
Object[] array = getArray();
synchronized (array) {
array.notify();
}
Multiple Waiters
It is possible for multiple threads to be waiting on the same object. This can
happen when multiple threads wait for the same event. For example, recall the Buffer
class described earlier; the buffer was operated on by a single Producer
and a single Consumer. What would happen if there were multiple Producers?
If the buffer filled, different Producers might attempt to use put()
to place items into the buffer; they would all block inside the put() method,
waiting for a Consumer to come along and free up space in the buffer.
When you call notify(), there may be zero, one, or more threads blocked
in a wait() on the monitor. If there are no threads waiting, the call to
notify() is a no-op--it does not affect any other threads. If there is a
single thread in wait(), that thread is notified and begins waiting for
the monitor to be released by the thread that called notify(). If two or
more threads are in a wait(), the virtual machine picks a single waiting
thread and notifies that thread. (The method used to "pick" a waiting thread
varies from platform to platform--your programs should not rely on the VM to select
a specific thread from the pool of waiting threads.)
Using notifyAll()
In some situations, you may want to notify every thread currently waiting on an
object. The Object API provides a method to do this: notifyAll().
The notify() method wakes only a single waiting thread, but the notifyAll()
method wakes every thread currently waiting on the object.
When would you want to use notifyAll()? Consider the java.awt.MediaTracker
class. This class is used to track the status of images being loaded over the network.
Multiple threads may wait on the same MediaTracker object, waiting for all
the images to be loaded. When the MediaTracker detects that all images have
been loaded, notifyAll() is called to inform every waiting thread that the
images have been loaded. notifyAll() is used because the MediaTracker
does not know how many threads are waiting; if notify() were used, some
of the waiting threads may not receive notification that the transfer was completed.
These threads would continue waiting, probably hanging the entire applet.
Listing 6.6, earlier in this chapter, can also benefit from the use of notifyAll().
In that code, the Buffer class used the notify() method to send
a notification to a single thread waiting on an empty or a full buffer. However,
there was no guarantee that only a single thread was waiting; multiple threads may
have been waiting for the same condition. Listing 6.7 shows a modified version of
the Buffer class (named Buffer2) that uses notifyAll().
Listing 6.7. The Buffer2 class, using notifyAll().
public class Buffer2 {
private char[] buf; // storage
private int last = 0; // last occupied position
private int writers_waiting = 0; // # of threads waiting in put()
private int readers_waiting = 0; // # of threads waiting in get()
public Buffer2(int sz) {
buf = new char[sz];
}
public boolean isFull() { return (last == buf.length); }
public boolean isEmpty() { return (last == 0); }
public synchronized void put(char c) {
while(isFull()) {
try { writers_waiting++; wait(); }
catch (InterruptedException e) { }
finally { writers_waiting--; }
}
buf[last++] = c;
if (readers_waiting > 0) {
notifyAll();
}
}
public synchronized char get() {
while(isEmpty()) {
try { readers_waiting++; wait(); }
catch (InterruptedException e) { }
finally { readers_waiting--; }
}
char c = buf[0];
System.arraycopy(buf, 1, buf, 0, --last);
if (writers_waiting > 0) {
notifyAll();
}
return c;
}
}
The get() and put() methods have been made more intelligent.
They now check to see whether any notification is necessary and then use notifyAll()
to broadcast an event to all waiting threads.
Summary
This chapter was a whirlwind tour of multithreaded programming in Java. Among
other things, the chapter covered the following:
- Creating your own thread classes by subclassing Thread or implementing
Runnable
- Using the ThreadGroup class to manage groups of threads
- Understanding thread states and thread scheduling
- Making your classes thread-safe by using the synchronized keyword to
protect objects from concurrent modification
- Understanding how monitors affect concurrent programming in Java
- Coordinating the actions of multiple threads by calling the wait() and
notify() methods
Java threads are not difficult to use. After reading this chapter, you should
begin to see how threads can be used to improve your everyday Java programming.
|