by Eric Williams
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:
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. |
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. |
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.
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.
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.
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();
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).
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:
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 following sections present a detailed analysis of the Java Thread API.
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:
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. |
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.
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). |
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:
By using these techniques, your applications and applets will be well behaved on any Java platform.`
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.
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. |
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).
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. |
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.
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. |
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.
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();
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.
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.
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.
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.
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();
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.
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:
The ThreadGroup class has security (as implemented by the current SecurityManager object) implemented for the following methods:
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.
Thread 1 | ||
cnt = counter.incr(); | ||
n = count; // 0 | ||
count = n + 1; // 1 | ||
return n; // 0 | ||
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.
Thread 1 | ||
cnt = counter.incr(); | ||
n = count; // 0 | ||
cnt = counter.incr(); | ||
n = count; // 0 | ||
count = n + 1; // 1 | ||
return n; // 0 | ||
count = n + 1; // 1 | ||
return n; // 0 |
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.
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:
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.
Thread 1 | Thread 2 | |
cnt = counter.incr(); | ||
(acquires the monitor) | ||
n = count; // 0 | ||
cnt = counter.incr(); | ||
(can't acquire monitor) | ||
count = n + 1; // 1 | (blocked) | |
return n; // 0 | (blocked) | |
(releases the monitor) | (blocked) | |
(acquires the monitor) | ||
n = count; // 1 | ||
count = n + 1; // 2 | ||
return n; // 1 | ||
(releases the monitor) |
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. |
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.
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.
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.) |
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.
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.
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.
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:
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().
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:
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 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:
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. |
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.
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(); }
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.)
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:
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.