All Categories :
Java
Chapter 9
Threads and Multithreading
by Eric Williams
CONTENTS
One of the characteristics that makes Java a powerful programming
language is its support of multithreaded programming as an integrated
part of the language. This 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 ability to perform
multiple jobs concurrently. For the most part, modern desktop
operating systems like Windows 95 or OS/2 have the ability to
run two or more programs at the same time. While you are using
Netscape to download a big file, you can be playing 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 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 does demonstrate the use of
a single Java thread. When a Java application begins, the 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 counts it.
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 to do multiple things at the same
time. For example, you may want to retrieve an image over the
network at the same time you are requesting an updated stock report,
and you also want to run several animations-all concurrently.
This kind of situation is where 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
9.1 modifies the preceding single-threaded application to take
advantage of threads. Instead of counting from 0 to 999, 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 about understanding the details of this example yet; it
is presented only to introduce you to threads.
Listing 9.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 9.1 shows the threads in the CountThreadTest
application.
Figure 9.1: Parallel Java threads.
Note |
Even though threads make it appear that a program is performing multiple 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, threads give the appearance of performing multiple tasks simultaneously by scheduling each thread to run one at a time, occasionally switching between threads. 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 a C programming 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. When you use Java threads, you do
not have to worry about what threading packages are available
on the under-lying 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, like main().
The main() method is static
because an application starts with only one main().
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
In this section, we'll discuss how to create a new thread by subclassing
java.lang.Thread. The Thread
class is the objectification of a Java sequence of control.
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. Because 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 9.2 contains this logic.
Listing 9.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. The first thing to note is that 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, 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 ends (as
does the concurrent execution of the thread). This process is
shown in Figure 9.2.
Figure 9.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 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
...
}
Using 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 concurrent 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 9.3).
Figure 9.3: Thread states.
When a Thread object is first
created, it is NEW. 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 object
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
must also share the CPU with other programs running on the system.
How a thread is allocated CPU time is covered in greater depth
in "Scheduling and Priority," later in this chapter.
When certain events happen in 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 these:
- 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 its state
changes (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-are immediately eligible for reclamation by the garbage collector (if, of course, they are not referenced elsewhere). Programmers are responsible for cleaning up system resources (open files, 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 three constructors, you must understand
the 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 the chapter.)
Constructing a new thread does not begin the execution of that
thread. To launch the Thread
object, you must invoke its start()
method.
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 threads 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 will be 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 may
return from its run() method,
ending gracefully. Or the thread may 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 9.3 shows how calling stop()
on a running thread generates a ThreadDeath
exception.
Listing 9.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 9.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, releasing 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 in Listing 9.3). Others prefer to use catch(Throwable t) exception handlers. Both of 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 Developers Kit, version 1.0.2. 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 is able to 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 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 preemptive, 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 may 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 preemptive scheduler can be either time-sliced
or nontime-sliced. With time-sliced scheduling, the scheduler
allocates a period of time that 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.
The current implementation of the Java virtual machine uses different
thread packages on different platforms; the behavior of the Java
thread scheduler differs slightly for each platform. The Java
implementation on Windows 95/NT uses the underlying Win32 thread
scheduler (which is time-sliced). On Solaris and other
UNIX platforms, the 1.0.2 JDK uses a custom package developed
by Sun called Green Threads (which is not time-sliced).
In the future, the Solaris JDK will likely use the Solaris thread
package (which is also 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 block 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) { }. This code has no purpose in the first
place. 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 may 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 isInterrupted()method.
Thread.interrupt() is the
same thing as Thread.currentThread().isInterrupted().
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 1.0.x JDK. 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.
|
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 1.0.2 JDK, 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 1.0.2 JDK, the join(int millisecond, int nanosecond) method uses the nano- second 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 9.4
is a graphical representation of the ThreadGroup
tree.
Figure 9.4: 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.
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 will be able to 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 threads or
groups in this immediate ThreadGroup
object are retrieved. The enumerate()
methods lacking the recurse
parameter perform the same as the enumerate()
method 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. 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 a critical system resource
that can 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, 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(ThreadGroup group, Runnable
target, String name)
- Thread(ThreadGroup group, 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()
One of the most powerful features of the Java programming language
is its ability to 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 may
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 can 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 9.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 9.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 9.2 describes a somewhat different sequence of execution.
In this case, 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 9.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 9.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 9.2 is called a race condition-the
outcome of the program was affected by the order in which the
program's threads were 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 language often confuse monitors with critical sections. Java monitors are not like traditional critical sections. Declaring a method as synchronized does not imply that only one thread at a time may execute that method, as would be the case with a critical section. It implies that only one thread may 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 9.2 (the race
condition)? The outcome of the same sequence of context switches
is listed in Table 9.3.
Table 9.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 9.3, the sequence of operations begins the same as the
earlier scenario. 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 becomes able to 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 was serialized by the addition of the synchronized
keyword. Generally speaking, any method that modifies an object's
attributes should be synchronized.
It is easy to mark all object-modifying methods as synchronized
and be done with it.
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.
|
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 may be
executing 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 9.4.
Listing 9.4. The Account
class.
class Account {
private int balance;
public Account(int balance) {
this.balance = balance;
}
public synchronized void transfer(int amount, Account 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. The Account
class 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 Account
objects 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 1.0 Java Developers Kit (JDK)
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
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 monitors. This section
presents 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 methods that do not have an associated object (static
methods)?
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 method
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).
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 it 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 1.0.1 JDK 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.
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, they are unable to proceed. In the simplest case, two
threads are each trying to acquire a monitor 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, each never
releases its monitor to the other thread.)
The sample application in Listing 9.5 should give you an understanding
of how a deadlock happens.
Listing 9.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 9.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 where the Java application is running. This 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.0 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 C and C++
look and feel. Regardless, volatile
is useless-use synchronized
methods rather than the volatile
keyword.
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 can also
be 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
may depend on another thread to complete an operation or to service
a request. For example, a spreadsheet program may 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 two 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 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 means to determine
how the wait() was ended-whether
a notify() occurred or whether
it 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 1.0.2 JDK, the wait(int millisecond, int nanosecond) method uses the nano- second 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 9.6 shows
the Buffer class in its entirety.
Listing 9.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 1.0 JDK, you can examine these classes to see how they handle interthread coordination.
|
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 threds
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
put() 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 would not receive notification
that the transfer was completed. These threads would continue
waiting, probably hanging the entire applet.
Listing 9.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 9.7 shows a modified version of the Buffer
class (named Buffer2) that
uses notifyAll().
Listing 9.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.
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.
Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.