TOC
BACK
FORWARD
HOME

Java 1.1 Unleashed

- 6 -
Threads and Multithreading

by Eric Williams

IN THIS CHAPTER

  • Multithreading
  • Concurrency
  • Advanced Monitor Concepts
  • Synchronization

Multithreading

One of the characteristics that makes Java a powerful programming language is its support for multithreaded programming as an integrated part of the language. This provision is unique because most modern programming languages either do not offer multithreading or provide multithreading as a nonintegrated package. Java, however, offers a single, integrated view of multithreading.

Multithreaded programming is an essential aspect of programming in Java. To master the Java programming language, you should first become familiar with the concepts of multithreaded programming. Then you should learn how multithreaded and concurrent programming are done in Java.

This chapter presents a complete introduction and reference to Java threads, including these topics:

  • How to write and start your own threads

  • A comprehensive reference to the Thread and ThreadGroup classes

  • How to make your classes thread safe

  • An introduction to Java monitors

  • How to coordinate the actions of multiple threads


NOTE: Multithreading and concurrent programming are unfamiliar concepts for most new Java programmers. If you are familiar with only single-threaded languages like Visual Basic, Delphi, Pascal, Cobol, and so on, you may be worried that threads are too hard to learn. Although learning to use Java threads is not trivial, the model is simple and easy to understand. Threads are a normal everyday aspect of developing Java applications and applets.

What Is a Thread?

In the early days of computing, computers were single tasking--that is, they ran a single job at a time. The big, lumbering machine would start one job, run that job to completion, then start the next job, and so on. When engineers became overly frustrated with these batch-oriented systems, they rewrote the programs that ran the machines and thus was born the modern multitasking operating system.

Multitasking refers to a computer's capability to perform multiple jobs concurrently. For the most part, modern operating systems like Windows 95 or Solaris can run two or more programs at the same time. While you are using Netscape to download a big file, you can be running Solitaire in a different window; both programs are running at the same time.

Multithreading is an extension of the multitasking paradigm. But rather than multiple programs, multithreading involves multiple threads of control within a single program. Not only is the operating system running multiple programs, each program can run multiple threads of control--think of threads as subprograms--within the program. For example, using a Web browser, you can print one Web page, download another, and fill out a form in a third--all at the same time.

A thread is a single sequence of execution within a program. Until now, you have probably used Java to write single-threaded applications, something like this:

class MainIsRunInAThread {
    public static void main(String[] args) {
        // main() is run in a single thread
        System.out.println(Thread.currentThread());
        for (int i=0; i<1000; i++) {
            System.out.println("i == " + i);
        }
    }

}

This example is simplistic, but it demonstrates the use of a single Java thread. When a Java application begins, the virtual machine (VM) runs the main() method inside a Java thread. (You have already used Java threads and didn't even know it!) Within this single thread, this simple application's main() method counts from 0 to 999, printing out each value as it is counted.

Programming within a single sequence of control can limit your ability to produce usable Java software. (Imagine using an operating system that could execute only one program at a time, or a Web browser that could load only a single page at a time.) When you write a program, you often want the program to do multiple things at the same time. For example, you may want the program to retrieve an image over the network at the same time it is requesting an updated stock report and also running several animations--and you want all this to occur concurrently. This is the kind of situation in which Java threads become useful.

Java threads allow you to write programs that do many things at once. Each thread represents an independently executing sequence of control. One thread can write a file out to disk while a different thread responds to user keystroke events.

Before jumping into the details about Java threads, let's take a peek at what a multithreaded application looks like. Listing 6.1 modifies the preceding single-threaded application to take advantage of threads. Instead of counting from 0 to 999 in one thread, this application uses five different threads to count from 0 to 999--each thread counts 200 numbers: 0 to 199, 200 to 399, and so on. Don't worry if you don't understand the details of this example yet; it is presented only to introduce you to threads.

Listing 6.1. A simple multithreaded application.

class CountThreadTest extends Thread {
    int from, to;
    public CountThreadTest(int from, int to) {
        this.from = from;
        this.to = to;
    }
    // the run() method is like main() for a thread
    public void run() {
        for (int i=from; i<to; i++) {
            System.out.println("i == " + i);
        }
    }
    public static void main(String[] args) {
        // spawn 5 threads, each of wich counts 200 numbers
        for (int i=0; i<5; i++) {
            CountThreadTest t = new CountThreadTest(i*200, (i+1)*200);
            // starting a thread will launch a separate sequence
            // of control and execute the run() method of the thread
            t.start();
        }
    }
}

When this application starts, the VM invokes the main() method in its own thread. main() then starts five separate threads to perform the counting operations. Figure 6.1 shows the threads in the CountThreadTest application.

Figure 6.1.

Parallel Java threads.


NOTE: Even though multiple threads may appear to perform tasks at the same time, technically speaking, this may not be true. Even today, most computers are equipped with a single processor--such computers can perform at most one task at a time. On single-processor systems, the operating system continuously switches between different tasks and threads, allowing each active task or thread to use the CPU for a small amount of time. This subject is discussed in detail in "Thread Scheduling," later in this chapter.

Java Threads

Support for multiple threads of execution is not a Java invention. Threads have been around for a long time and have been implemented in many programming languages. However, programmers have had to struggle with a lack of thread standards. Different platforms have different thread packages, each with a different API. Operating systems do not have uniform support for threads; some support threads in the OS kernel, and some do not. Only recently has a standard emerged for threads--POSIX threads (IEEE standard 1003.1c-1995). However, the POSIX threads standard defines only a C programming interface (not a Java interface) and is not yet widely implemented.

One of the greatest benefits of Java is that it presents the Java programmer with a unified multithreading API--one that is supported by all Java virtual machines on all platforms. When you use Java threads, you do not have to worry about which threading packages are available on the underlying platform or whether the operating system supports kernel threads. The virtual machine isolates you from the platform-specific threading details. The Java threading API is identical on all Java implementations.

Creating New Threads

The first thing you need to know about threads is how to create and run a thread. This process involves two steps: writing the code that is executed in the thread and writing the code that starts the thread.

As discussed earlier, you are already familiar with how to write single-threaded programs. When you write a main() function, that method is executed in a single thread. The Java virtual machine provides a multithreaded environment, but it starts user applications by calling main() in a single thread.

An application's main() method provides the central logic for the main thread of the application. Writing the code for a thread is similar to writing main(). You must provide a method that implements the main logic of the thread. This method is always named run() and has the following signature:

public void run();

Notice that the run() method is not a static method as main() is. The main() method is static because an application starts with only one main() method. But an application may have many threads, so the main logic for a thread is associated with an object--the Thread object.

You can provide an implementation for the run() method in two ways. Java supports the run() method in subclasses of the Thread class. Java also supports run() through the Runnable interface. Both methods for providing a run() method implementation are described in the following sections.

Subclassing the Thread Class

This section discusses how to create a new thread by subclassing java.lang.Thread.

Let's start with a plausible situation in which a thread might be useful. Suppose that you are building an application; in one part of this application, a file must be copied from one directory to a different directory. But when you run the application, you find that if the file is large, the application stalls during the time that the file is being copied. You determine that the cause of the stall is this: When the application is copying the file, it is unable to respond to user-interface events.

To improve this situation, you decide that the file-copy operation should be performed concurrently, in a separate thread. To move this logic to a thread, you provide a subclass of the Thread class that contains this logic, implemented in the run() method. The FileCopyThread class shown in Listing 6.2 contains this logic.

Listing 6.2. The file-copy logic in FileCopyThread.

// subclass from Thread to provide your own kind of Thread
class FileCopyThread extends Thread {
    private File from;
    private File to;
    public FileCopyThread(File from, File to) {
        this.from = from;
        this.to = to;
    }
    // implement the main logic of the thread in the run()
    // method [run() is equivalent to an application's main()]
    public void run() {
        FileInputStream in = null;
        FileOutputStream out = null;
        byte[] buffer = new byte[512];
        int size = 0;
        try {
            // open the input and output streams
            in = new FileInputStream(from);
            out = new FileOutputStream(to);
            // copy 512 bytes at a time until EOF
            while ((size = in.read(buffer)) != -1) {
               out.write(buffer, 0, size);
            }
        } catch(IOException ex) {
            ex.printStackTrace();
        } finally {
            // close the input and output streams
            try {
                if (in != null) { in.close(); }
                if (out != null) { out.close(); }
            } catch (IOException ex) {
            }
        }
    }

}

Let's analyze the FileCopyThread class. Note that the FileCopyThread subclasses from Thread. By subclassing from Thread, FileCopyThread inherits all the state and behavior of a Thread--the property of "being a thread."

The FileCopyThread class implements the main logic of the thread in the run() method. (Remember that the run() method is the initial method for a Java thread, just as the main() method is the initial method for a Java application.) Within run(), the input file is copied to the output file in 512-byte chunks. When a FileCopyThread instance is created and started, the entire run() method is executed in one separate sequence of control (you'll see how this is done soon).

Now that you are familiar with how to write a Thread subclass, you have to learn how to use that class as a separate control sequence within a program. To use a thread, you must start the concurrent execution of the thread by calling the Thread object's start() method. The following code demonstrates how to launch a file-copy operation as a separate thread:

File from = getCopyFrom();
File to = getCopyTo();
// create an instance of the thread class
Thread t = new FileCopyThread(from, to);
// call start() to activate the thread asynchronously
t.start();

Invoking the start() method of a FileCopyThread object begins the concurrent execution of that thread. When the thread starts running, its run() method is called. In this case, the file copy begins its execution concurrently with the original thread. When the file copy is finished, the run() method returns (and the concurrent execution of the thread ends). This process is shown in Figure 6.2.

Figure 6.2.

Concurrent file copy.

Implementing the Runnable Interface

There are situations in which it is not convenient to create a Thread subclass. For example, you may want to add a run() method to a preexisting class that does not inherit from Thread. The Java Runnable interface makes this possible.

The Java threading API supports the notion of a thread-like entity that is an interface: java.lang.Runnable. Runnable is a simple interface, with only one method:

public interface Runnable {
    public void run();
}

This interface should look familiar. In the previous section, we covered the Thread class, which also supported the run() method. To subclass Thread, we redefined the Thread run() method. To use the Runnable interface, you must write a run() method and add the text implements Runnable to the class. Reimplementing the FileCopyThread (of the previous example) as a Runnable interface requires few changes:

// implementing Runnable is a different way to use threads
class FileCopyRunnable implements Runnable {
    // the rest of the class remains mostly the same
    ...
}

To use a Runnable interface as a separate control sequence requires the cooperation of a Thread object. Although the Runnable object contains the main logic, Thread is the only class that encapsulates the mechanism of launching and controlling a thread. To support Runnable, a separate Runnable parameter was added to several of the Thread class constructors. A thread that has been initialized with a Runnable object will call that object's run() method when the thread begins executing.

Here is an example of how to start a thread using FileCopyRunnable:

File from = new File("file.1");
File to = new File("file.2");
// create an instance of the Runnable
Runnable r = new FileCopyRunnable(from, to);
// create an instance of Thread, passing it the Runnable
Thread t = new Thread(r);
// start the thread
t.start();

Thread States

Although you have learned a few things about threads, we have not yet discussed one aspect that is critical to your understanding of how threads work in Java--thread states. A Java thread, represented by a Thread object, traverses a fixed set of states during its lifetime (see Figure 6.3).

Figure 6.3.

Thread states.

When a Thread object is first created, it is in the NEW state. At this point, the thread is not executing. When you invoke the Thread's start() method, the thread changes to the RUNNABLE state.

When a Java thread is RUNNABLE, it is eligible for execution. However, a thread that is RUNNABLE is not necessarily running. RUNNABLE implies that the thread is alive and that it can be allocated CPU time by the system when the CPU is available--but the CPU may not always be available. On single-processor systems, Java threads must share the single CPU; additionally, the Java virtual machine task (or process) must also share the CPU with other tasks running on the system. How a thread is allocated CPU time is covered in greater depth in "Scheduling and Priority," later in the chapter.

When certain events happen to a RUNNABLE thread, the thread may enter the NOT RUNNABLE state. When a thread is NOT RUNNABLE, it is still alive, but it is not eligible for execution. The thread is not allocated time on the CPU. Some of the events that may cause a thread to become NOT RUNNABLE include the following:

  • The thread is waiting for an I/O operation to complete

  • The thread has been put to sleep for a certain period of time (using the sleep() method)

  • The wait() method has been called (as discussed in "Synchronization," later in this chapter)

  • The thread has been suspended (using the suspend() method)

A NOT RUNNABLE thread becomes RUNNABLE again when the condition that caused the thread to become NOT RUNNABLE ends (I/O has completed, the thread has ended its sleep() period, and so on). During the lifetime of a thread, the thread may frequently move between the RUNNABLE and NOT RUNNABLE states.

When a thread terminates, it is said to be DEAD. Threads can become DEAD in a variety of ways. Usually, a thread dies when its run() method returns. A thread may also die when its stop() or destroy() method is called. A thread that is DEAD is permanently DEAD--there is no way to resurrect a DEAD thread.


NOTE: When a thread dies, all the resources consumed by the thread--including the Thread object itself--become eligible for reclamation by the garbage collector (if, of course, they are not referenced elsewhere). Programmers are responsible for cleaning up system resources (closing open files, disposing of graphics contexts, and so on) while a thread is terminating, but no cleanup is required after a thread dies.

The Thread API

The following sections present a detailed analysis of the Java Thread API.

Constructors

The Thread class has seven different constructors:

public Thread();
public Thread(Runnable target);
public Thread(Runnable target, String name);
public Thread(String name);
public Thread(ThreadGroup group, Runnable target);
public Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, String name);

These constructors represent most of the combinations of three different parameters: thread name, thread group, and a Runnable target object. To understand the constructors, you must understand the three parameters:

  • name is the (string) name to be assigned to the thread. If you fail to specify a name, the system generates a unique name of the form Thread-N, where N is a unique integer.

  • target is the Runnable instance whose run() method is executed as the main method of the thread.

  • group is the ThreadGroup to which this thread will be added. (The ThreadGroup class is discussed in detail later in this chapter.)

Constructing a new thread does not begin the execution of that thread. To launch the Thread object, you must invoke its start() method.

When creating a thread, the priority and daemon status of the new thread are set to the same values as the thread from which the new thread was created.


CAUTION: Although it is possible to allocate a thread using new Thread(), it is not useful to do so. When constructing a thread directly (without subclassing), the Thread object requires a target Runnable object because the Thread class itself does not contain your application's logic.

Naming

public final String getName();

public final void setName(String name);

Every Java thread has a name. The name can be set during construction or with the setName() method. If you fail to specify a name during construction, the system generates a unique name of the form Thread-N, where N is a unique integer; the name can be changed later using setName().

The name of a thread can be retrieved using the getName() method.

Thread names are important because they provide the programmer with a useful way to identify particular threads during debugging. You should name a thread in such a way that you (or others) will find the name helpful in identifying the purpose or function of the thread during debugging.

Starting and Stopping

To start and stop threads once you have created them, you need the following methods:

public void start();
public final void stop();
public final void stop(Throwable obj);
public void destroy();

To begin a new thread, create a new Thread object and call its start() method. An exception is thrown if start() is called more than once on the same thread.

As discussed in "Thread States," earlier in this chapter, there are two main ways a thread can terminate: The thread can return from its run() method, ending gracefully. Or the thread can be terminated by the stop() or destroy() method.

When invoked on a thread, the stop() method causes that thread to terminate by throwing an exception to the thread (a ThreadDeath exception). Calling stop() on a thread has the same behavior as executing throw new ThreadDeath() within the thread, except that stop() can also be called from other threads (whereas the throw statement affects only the current thread).

To understand why stop() is implemented this way, consider what it means to stop a running thread. Active threads are part of a running program, and each runnable thread is in the middle of doing something. It is likely that each thread is consuming system resources: file descriptors, graphics contexts, monitors (to be discussed later), and so on. If stopping a thread caused all activity on the thread to cease immediately, these resources might not be cleaned up properly. The thread would not have a chance to close its open files or release the monitors it has locked. If a thread were stopped at the wrong moment, it would be unable to free these resources; this leads to potential problems for the virtual machine (running out of open file descriptors, for example).

To provide for clean thread shutdown, the thread to be stopped is given an opportunity to clean up its resources. A ThreadDeath exception is thrown to the thread, which percolates up the thread's stack and through the exception handlers that are currently on the stack (including finally blocks). Monitors are also released by this stack-unwinding process.

Listing 6.3 shows how calling stop() on a running thread generates a ThreadDeath exception.

Listing 6.3. Generating a ThreadDeath exception with stop().

class DyingThread extends Thread {
    // main(), this class is an application
    public static void main(String[] args) {
        Thread t = new DyingThread();          // create the thread
        t.start();                             // start the thread
        // wait for a while
        try { Thread.sleep(100); } catch (InterruptedException e) { }
        t.stop();                              // now stop the thread
    }
    // run(), this class is also a Thread
    public void run() {
        int n = 0;
        PrintStream ps = null;
        try {
            ps = new PrintStream(new FileOutputStream("big.txt"));
            while (true) {                           // forever
                ps.println("n == " + n++);
                try { Thread.sleep(5); } catch (InterruptedException e) { }
            }
        } catch (ThreadDeath td) {                 // watch for the stop()
            System.out.println("Cleaning up.");
            ps.close();                            // close the open file
            // it is very important to rethrow the ThreadDeath
            throw td;
        } catch (IOException e) {
        }
    }

}

The DyingThread class has two parts. The main() method spawns a new DyingThread, waits for a period of time, and then sends a stop() to the thread. The DyingThread run() method, which is executed in the spawned thread, opens a file and periodically writes output to that file. When the thread receives the stop(), it catches the ThreadDeath exception and closes the open file. It then rethrows the ThreadDeath exception.

When you run the code shown in Listing 6.3, you see the following output:

Cleaning up.


NOTE: Java provides a convenient mechanism for programmers to write "cleanup" code--code that is executed when errors occur or when a program or thread terminates. (Cleanup involves closing open files, disposing of graphics contexts, hiding windows, and so on.) Exception handler catch and finally blocks are good locations for cleanup code.

Programmers use a variety of styles to write cleanup code. Some programmers place cleanup code in catch(ThreadDeath td) exception handlers (as was done in Listing 6.3). Others prefer to use catch(Throwable t) exception handlers. Both these methods are good, but writing cleanup code in a finally block is the best solution for most situations. A finally block is executed unconditionally, whether the exception handler exited because of a thrown exception or not. If an exception was thrown, it is automatically rethrown after the finally block has completed.

Although the ThreadDeath solution allows the application a high degree of flexibility, there are problems. By catching the ThreadDeath exception, a thread can actually prevent stop() from having the desired effect. The code to do this is trivial:

// prevent stop() from working
catch (ThreadDeath td) {
    System.err.println("Just try to stop me. I'm invincible.");
    // oh no, I've failed to rethrow td
}

Calling stop() is not sufficient to guarantee that a thread will end. This is a serious problem for Java-enabled Web browsers; there is no guarantee that an applet will terminate when stop() is invoked on a thread belonging to the applet.

The destroy() method is stronger than the stop() method. The destroy() method is designed to terminate the thread without resorting to the ThreadDeath mechanism. The destroy() method stops the thread immediately, without cleanup; any resources held by the thread are not released.


CAUTION: The destroy() method is not implemented in the Java Development Kit, in all versions up to and including 1.1. Calling this method results in a NoSuchMethodError exception. Although there has been no comment about when this method will be implemented, it is likely that it will not become available until JavaSoft can implement it in a way that cleans up the dying thread's environment (locked monitors, pending I/O, and so on).

Scheduling and Priority

Thread scheduling is defined as the mechanism used to determine how RUNNABLE threads are allocated CPU time (that is, when they actually get to execute for a period of time on the computer's CPU). In general, scheduling is a complex subject that uses terms such as pre- emptive, round-robin scheduling, priority-based scheduling, time-sliced, and so on.

A thread-scheduling mechanism is either preemptive or nonpreemptive. With preemptive scheduling, the thread scheduler preempts (pauses) a running thread to allow different threads to execute. A nonpreemptive scheduler never interrupts a running thread; instead, the nonpreemptive scheduler relies on the running thread to yield control of the CPU so that other threads can execute. Under nonpreemptive scheduling, other threads may starve (never get CPU time) if the running thread fails to yield.

Among thread schedulers classified as preemptive, there is a further classification. A pre- emptive scheduler can be either time-sliced or nontime-sliced. With time-sliced scheduling, the scheduler allocates a period of time for which each thread can use the CPU; when that amount of time has elapsed, the scheduler preempts the thread and switches to a different thread. A nontime-sliced scheduler does not use elapsed time to determine when to preempt a thread; it uses other criteria such as priority or I/O status.

Different operating systems and thread packages implement a variety of scheduling policies. But Java is intended to be platform independent. The correctness of a Java program should not depend on what platform the program is running on, so the designers of Java decided to isolate the programmer from most platform dependencies by providing a single guarantee about thread scheduling: The highest priority RUNNABLE thread is always selected for execution above lower priority threads. (When multiple threads have equally high priorities, only one of those threads is guaranteed to be executing.)

Java threads are guaranteed to be preemptive, but not time sliced. If a higher priority thread (higher than the current thread) becomes RUNNABLE, the scheduler preempts the current thread. However, if an equal or lower priority thread becomes RUNNABLE, there is no guarantee that the new thread will ever be allocated CPU time until it becomes the highest priority RUNNABLE thread.


NOTE: The current implementation of the Java VM uses different thread packages on different platforms; thus, the behavior of the Java thread scheduler varies slightly from platform to platform. It is best to check with your Java VM supplier to determine whether the VM uses native threads and whether the platform's native threads are time sliced (some native threading packages, most notably Solaris threads, are not time sliced).

Even though Java threads are not guaranteed to be time sliced, this should not be a problem for the majority of Java applications and applets. Java threads release control of the CPU when they become NOT RUNNABLE. If a thread is waiting for I/O, is sleeping, or is waiting to enter a monitor, the thread scheduler will select a different thread for execution. Generally, only threads that perform intensive numerical analysis (without I/O) will be a problem. A thread would have to be coded like the following example to prevent other threads from running (and such a thread would starve other threads only on some platforms--on Windows NT, for example, other threads would still be allowed to run):

int i = 0;
while (true) {
i++;
}

There are a variety of techniques you can implement to prevent one thread from consuming too much CPU time:

  • Don't write code such as while (true) { }. It is acceptable to have infinite loops--as long as what takes place inside the loop involves I/O, sleep(), or interthread coordination (using the wait() and notify() methods, discussed later in this chapter).

  • Occasionally call Thread.yield() when performing operations that are CPU intensive. The yield() method allows the scheduler to spend time executing other threads.

  • Lower the priority of CPU-intensive threads. Threads with a lower priority run only when the higher priority threads have nothing to do. For example, the Java garbage collector thread is a low priority thread. Garbage collection takes place when there are no higher priority threads that need the CPU; this way, garbage collection does not needlessly stall the system.

By using these techniques, your applications and applets will be well behaved on any Java platform.

Setting Thread Priority

public final static int MAX_PRIORITY = 10;
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final int getPriority();
public final void setPriority(int newPriority);

Every thread has a priority. When a thread is created, it inherits the priority of the thread that created it. The priority can be adjusted subsequently using the setPriority() method. The priority of a thread can be obtained using getPriority().

There are three symbolic constants defined in the Thread class that represent the range of priority values: MIN_PRIORITY, NORM_PRIORITY, and MAX_PRIORITY. The priority values range from 1 to 10, in increasing priority. An exception is thrown if you attempt to set priority values outside this range.

Waking Up a Thread

public void interrupt();
public static boolean interrupted();
public boolean isInterrupted();

To send a wake-up message to a thread, call interrupt() on its Thread object. Calling interrupt() causes an InterruptedException to be thrown in the thread and sets a flag that can be checked by the running thread using the interrupted() or isInterrupted() method. Calling Thread.interrupted() checks the interrupt status of the current thread and resets the interrupt status to false (in versions before 1.1, the interrupt status is not reset). Calling isInterrupted() on a Thread object (which can be other than the current thread) checks the interrupt status of that thread but does not change the status.

The interrupt() method is useful in waking a thread from a blocking operation such as I/O, wait(), or an attempt to enter a synchronized method.


CAUTION: The interrupt() method is not fully implemented in the JDK 1.0.x. Calling interrupt() on a thread sets the interrupted flag but does not throw an InterruptedException or end a blocking operation in the target thread; threads must check interrupted() to determine whether the thread has been interrupted.

The interrupt() method is fully implemented in the Java virtual machine, version 1.1.

Suspending and Resuming Thread Execution

public final void suspend();
public final void resume();

Sometimes, it is necessary to pause a running thread. You can do so using the suspend() method. Calling the suspend() method ensures that a thread will not be run. The resume() method reverses the suspend() operation.

A call to suspend() puts the thread in the NOT RUNNABLE state. However, calling resume() does not guarantee that the target thread will become RUNNABLE; other events may have caused the thread to be NOT RUNNABLE (or DEAD).

Putting a Thread to Sleep

public static void sleep(long millisecond);
public static void sleep(long millisecond, int nanosecond);

To pause the current thread for a specified period of time, call one of the varieties of the sleep() method. For example, Thread.sleep(500) pauses the current thread for half a second, during which time the thread is in the NOT RUNNABLE state. When the specified time expires, the current thread again becomes RUNNABLE.


CAUTION: In the JDK versions 1.0.x and 1.1, the sleep(int millisecond, int nanosecond) method uses the nanosecond parameter to round the millisecond parameter to the nearest millisecond. Sleeping is not yet supported in nanosecond granularity.

Making a Thread Yield

public static void yield();

The yield() method is used to give a hint to the thread scheduler that now would be a good time to run other threads. If many threads are RUNNABLE and waiting to execute, the yield() method is guaranteed to switch to a different RUNNABLE thread only if the other thread has at least as high a priority as the current thread.

Waiting for a Thread to End

public final void join();
public final void join(long millisecond);
public final void join(long millisecond, int nanosecond);

Programs sometimes have to wait for a specific thread to terminate; this is referred to as joining the thread. To wait for a thread to terminate, invoke one of the join() methods on its Thread object. For example:

Thread t = new OperationINeedDoneThread();
t.start();
.... // do some other stuff
t.join(); // wait for the thread to complete

The two join() methods with time parameters are used to specify a timeout for the join() operation. If the thread does not terminate within the specified amount of time, join() returns anyway. To determine whether a timeout has happened, or whether the thread has ended, use the Thread method isAlive().

join() with no parameters waits forever for the thread to terminate.


CAUTION: In the JDK versions 1.0.x and 1.1, the join(int millisecond, int nanosecond) method uses the nanosecond parameter to round the millisecond parameter to the nearest millisecond. Joining is not yet supported in nanosecond granularity.

Understanding Daemon Threads

public final boolean isDaemon();
public final void setDaemon(boolean on);

Some threads are intended to be "background" threads, providing service to other threads. These threads are referred to as daemon threads. When only daemon threads remain alive, the Java virtual machine process exits.

The Java virtual machine has at least one daemon thread, known as the garbage collection thread. The garbage collection thread is a low priority thread, executing only when there is nothing else for the system to do.

The setDaemon() method sets the daemon status of this thread. The isDaemon() method returns true if this thread is a daemon thread; it returns false otherwise.

Miscellaneous Thread Methods

The countStackFrames() method returns the number of active stack frames (method activations) currently on this thread's stack. The thread must be suspended when this method is invoked. Following is this method's signature:

public int countStackFrames();

The getThreadGroup() method returns the ThreadGroup class to which this thread belongs. A thread is always a member of a single ThreadGroup class. Following is this method's signature:

public final ThreadGroup getThreadGroup();

The isAlive() method returns true if start() has been called on this thread and if this thread has not yet died. In other words, isAlive() returns true if this thread is RUNNABLE or NOT RUNNABLE and false if this thread is NEW or DEAD. Following is this method's signature:

public final boolean isAlive();

The currentThread() method returns the Thread object for the current sequence of execution. Following is this method's signature:

public static Thread currentThread();

The activeCount() method returns the number of threads in the currently executing thread's ThreadGroup class. Following is this method's signature:

public static int activeCount();

The enumerate() method returns (through the tarray parameter) a list of all threads in the current thread's ThreadGroup class. Following is this method's signature:

public static int enumerate(Thread tarray[]);

The dumpStack() method is used for debugging. It prints a method-by-method list of the stack trace for the current thread to the System.err output stream. Following is this method's signature:

public static void dumpStack();

The toString() method returns a debugging string that describes this thread. Following is this method's prototype:

public String toString();

The ThreadGroup API

Each Java thread belongs to exactly one ThreadGroup instance. The ThreadGroup class is used to assist with the organization and management of similar groups of threads. For example, thread groups can be used by Web browsers to group all threads belonging to a single applet. Single commands can be used to manage the entire group of threads belonging to the applet.

ThreadGroup objects form a tree-like structure; groups can contain both threads and other groups. The top thread group is named system; it contains several system-level threads (such as the garbage collector thread). The system group also contains the main ThreadGroup object; the main group contains a main Thread--the thread in which main() is run. Figure 6.4 is a graphical representation of the ThreadGroup tree.

Constructors

The ThreadGroup class has two constructors. Both constructors require that you specify a name for the new thread group. One of the constructors takes a reference to the parent group of the new ThreadGroup; the constructor that does not take the parent parameter uses the group of the currently executing thread as the parent of the new group.

public ThreadGroup(String name);
public ThreadGroup(ThreadGroup parent, String name);

Initially, the new ThreadGroup object contains no threads or other thread groups.

Figure 6.4.

The ThreadGroup tree.

Thread Helper Methods

The ThreadGroup class contains a few methods that operate on the threads within the group. These methods are "helper" in nature; they invoke the same-named Thread method on all threads within the group (recursively, to thread groups within this group).

public final void suspend();
public final void resume();
public final void stop();
public final void destroy();

The helper methods include suspend(), resume(), stop(), and destroy(). Here is an example of how to stop an entire group of threads with a single method call:

ThreadGroup group = new ThreadGroup("client threads");
while (some_condition) {
    Thread t = new Thread(group);
    t.start();
    ...
}
...
if (kill_em_all) {   // stop all of the threads
    group.stop();
}

The other thread group helper methods can be called in a similar manner.

Priority

ThreadGroup trees can assist in the management of thread priority. After calling setMaxPriority() on a ThreadGroup object, no thread within the group's tree can use setPriority() to set a priority higher than the specified maximum value. (Priorities of threads already in the group are not affected.)

public final int getMaxPriority();
public final void setMaxPriority(int pri);

The getMaxPriority() method returns the maximum priority value of this ThreadGroup tree.

ThreadGroup Tree Navigation

Each thread group can contain both threads and thread groups. The activeCount() and activeCountGroup() methods return the number of contained threads and groups, respectively. Following are the method signatures:

public int activeCount();
public int activeGroupCount();

The activeCount() method returns the number of threads that are members of this ThreadGroup tree (recursively).

The activeCountGroup() method returns the number of ThreadGroups that are members of this ThreadGroup tree (recursively).

The following enumerate() methods can be used to retrieve the list of threads or groups in this ThreadGroup object:

public int enumerate(Thread list[]);
public int enumerate(Thread list[], boolean recurse);
public int enumerate(ThreadGroup list[]);
public int enumerate(ThreadGroup list[], boolean recurse);

The recurse parameter, if true, causes the retrieval of all the threads or groups within this ThreadGroup tree (recursively). If recurse is false, only the threads or groups in this immediate ThreadGroup object are retrieved. The enumerate() methods lacking the recurse parameter perform in the same way as the enumerate() methods with recurse set to true.

The parentOf() method returns true if this thread group is the parent of the specified group; it returns false otherwise. Following is this method's syntax:

public final boolean parentOf(ThreadGroup g);

The getParent() method returns the parent of this thread group, or null if this ThreadGroup is the top-level ThreadGroup. Following is this method's syntax:

public final ThreadGroup getParent();

The list() method prints debugging information about this ThreadGroup's tree (threads and groups) to System.out. Following is this method's syntax:

public void list();

Miscellaneous ThreadGroup Methods

The getName() method returns the name of this thread group. Following is this method's syntax:

public final String getName();

Some thread groups, like some threads, can be referred to as daemons. When a ThreadGroup object is a daemon group (setDaemon(true) has been called), the group is destroyed once all its threads and groups have been removed.

public final boolean isDaemon();
public final void setDaemon(boolean daemon);

The isDaemon() method returns true if this thread group is a daemon; it returns false otherwise.

The toString() method returns debugging information about this thread group. Following is this method's syntax:

public String toString();

When a thread exits because it failed to catch an exception, the uncaughtException() method of the thread's group is invoked with the Thread object and the exception (Throwable) as parameters:

public void uncaughtException(Thread t, Throwable e);

The default behavior of uncaughtException() is to pass the thread and exception to the parent of this thread group. The system thread group, if reached, calls the Throwable exception's printStackTrace() method, dumping the stack trace of the exception to System.err.

Security Features

Threads and thread groups are considered critical system resources that must be protected by Java's security features. The precise implementation of the security policy depends on the environment. When running a Java application, there is no security unless you install a SecurityManager using System.setSecurityManager(). Applets, however, use the SecurityManager installed by the browser environment. When you run an applet under Netscape Navigator 3.0, for example, the applet is allowed to modify only the threads and thread groups created by the current applet; attempts to modify other threads or groups result in a SecurityException.

The Thread class has security (as implemented by the current SecurityManager object) implemented for the following methods:

  • Thread(ThreadGroup group)

  • Thread(ThreadGroupgroup, Runnable target, String name)

  • Thread(ThreadGroupgroup, String name)

  • stop()

  • suspend( and resume()


  • setPriority
    )


  • setName
    )


  • setDaemon
    )

The ThreadGroup class has security (as implemented by the current SecurityManager object) implemented for the following methods:

  • ThreadGroup(ThreadGroup parent, String name)

  • setDaemon()

  • setMaxPriority()

  • stop()

  • suspend( and resume()

  • destroy()

Concurrency

One of the most powerful features of the Java programming language is that it can run multiple threads of control. Performing multiple tasks at the same time seems natural from the user's perspective--for example, simultaneously downloading a file from the Internet, performing a spreadsheet recalculation, and printing a document. From a programmer's point of view, however, managing concurrency is not as natural as it seems. Concurrency requires the programmer to take special precautions to ensure that Java objects are accessed in a thread-safe manner.

There is nothing obvious about threads that makes threaded programs unsafe; nevertheless, threaded programs can be subject to hazardous situations unless you take appropriate measures to make them safe.

The following example demonstrates how a threaded program can be unsafe:

public class Counter {
    private int count = 0;
    public int incr() {
        int n = count;
        count = n + 1;
        return n;
    }
}

As Java classes go, the Counter class is simple, having only one attribute and one method. As its name implies, the Counter class is used to count things, such as the number of times a button is pressed or the number of times the user visits a particular Web site. The incr() method is the heart of the class, returning and incrementing the current value of the counter. However, the incr() method has a problem; it is a source of unpredictable behavior in a multithreaded environment.

Consider a situation in which a Java program has two runnable threads, both of which are about to execute this line of code (affecting the same Counter object):

int cnt = counter.incr();

The programmer cannot predict or control the order in which these two threads are run. The Java thread scheduler has full authority over thread scheduling. There are no guarantees about which thread will receive CPU time, when the threads will execute, or how long each thread will be allowed to execute. Either thread may be interrupted by the scheduler at any time (remember that Java's thread scheduler is preemptive). On a multiprocessor machine, both threads may execute concurrently on separate processors.

Table 6.1 describes one possible sequence of execution of the two threads. In this scenario, the first thread is allowed to run until it completes its call to counter.incr(); then the second thread does the same. There are no surprises in this scenario. The first thread increments the Counter value to 1, and the second thread increments the value to 2.

Table 6.1. Counter scenario I.

Thread 1 Thread 2 Count
cnt = counter.incr(); --- 0
n = count; // 0 --- 0
count = n + 1; // 1 --- 1
return n; // 0 --- 1
--- cnt = counter.incr(); 1
--- n = count; // 1 1
--- count = n + 1; // 2 2
--- return n; // 1 2


Table 6.2 describes a somewhat different sequence of execution. In this scenario, the first thread is interrupted by a context switch (a switch to a different thread) during execution of the incr() method. The first thread remains temporarily suspended, and the second thread is allowed to proceed. The second thread executes its call to the incr() method, incrementing the Counter value to 1. When the first thread resumes, a problem becomes evident. The Counter's value is not updated to the value 2, as you would expect, but is instead set again to the value 1.

Table 6.2. Counter scenario II.

Thread 1 Thread 2 Count
cnt = counter.incr(); --- 0
n = count; // 0 --- 0
--- cnt = counter.incr(); 0
--- n = count; // 0 0
--- count = n + 1; // 1 1
--- return n; // 0 1
count = n + 1; // 1 --- 1
return n; // 0 --- 1


By examining Thread 1 in Table 6.2, you can see a problematic sequence of operations. After entering the incr() method, the value of the count attribute (0) is stored in a local variable, n. The thread is then suspended for a period of time while a different thread executes. (It is important to note that the count attribute is modified by the second thread during this time.) When Thread 1 resumes, it stores the value n + 1 (1) back in the count attribute. Unfortunately, this is no longer a correct value for the counter because the counter was already incremented to 1 by Thread 2.

The problem outlined by Table 6.2 is called a race condition--the outcome of the program is affected by the order in which the program's threads are allocated CPU time. It is usually considered inappropriate to allow race conditions to affect a program's result. Consider a medical device that monitors a patient's blood pressure. If this device were affected by race conditions in its software, it might report an incorrect reading to the physician. The physician would base medical treatment decisions on incorrect information--a bad situation for the patient, doctor, insurance company, and software vendor!

All multithreaded programs, even Java programs, can suffer from race conditions. Fortunately, Java provides the programmer with the necessary tools to manage concurrency--monitors.

Monitors

Many texts on computer science and operating systems deal with the issue of concurrent programming. Concurrency has been the subject of much research over the years, and many concurrency-control solutions have been proposed and implemented. These solutions include the following:

  • Critical sections

  • Semaphores

  • Mutexes

  • Database record locking

  • Monitors

Java implements a variant of the monitor approach to concurrency.

The concept of a monitor was introduced by C. A. R. Hoare in a 1974 paper published in the Communications of the ACM. Hoare described a special-purpose object, called a monitor, which applies the principle of mutual exclusion to groups of procedures (mutual exclusion is a fancy way of saying "one thread at a time"). In Hoare's model, each group of procedures requiring mutual exclusion is placed under the control of a single monitor. At run time, the monitor allows only one thread at a time to execute a procedure controlled by the monitor. If another thread tries to invoke a procedure controlled by the monitor, that thread is suspended until the first thread completes its call.

Java monitors remain true to Hoare's original concept, with a few minor variations (which are not discussed here). Monitors in Java enforce mutually exclusive access to methods; more specifically, Java monitors enforce mutually exclusive access to synchronized methods. (The synchronized keyword is an optional method modifier. If the synchronized keyword appears before the return type and signature of the method, the method is referred to as a "synchronized method.")

Every Java object has an associated monitor. synchronized methods that are invoked on an object use that object's monitor to limit concurrent access to that object. When a synchronized method is invoked on an object, the object's monitor is consulted to determine whether any other thread is currently executing a synchronized method on the object. If no other thread is executing a synchronized method on that object, the current thread is allowed to enter the monitor. (Entering a monitor is also referred to as locking the monitor, or acquiring ownership of the monitor.) If a different thread has already entered the monitor, the current thread must wait until the other thread leaves the monitor.

Metaphorically, a Java monitor acts as an object's gatekeeper. When a synchronized method is called, the gatekeeper allows the calling thread to pass and then closes the gate. While the thread is still in the synchronized method, subsequent synchronized method calls to that object from other threads are blocked. Those threads line up outside the gate, waiting for the first thread to leave. When the first thread exits the synchronized method, the gatekeeper opens the gate, allowing a single waiting thread to proceed with its synchronized method call. The process repeats.

In plain English, a Java monitor enforces a one-at-a-time approach to concurrency. This is also known as serialization (not to be confused with "object serialization," which is the Java library for reading and writing objects on a stream).


NOTE: Programmers already familiar with multithreaded programming in a different programming language often confuse monitors with critical sections. Java monitors are not like traditional critical sections. Declaring a method synchronized does not imply that only one thread at a time can execute that method, as is the case with a critical section. Monitors imply that only one thread can invoke that method (or any synchronized method) on a particular object at any given time. Java monitors are associated with objects, not with blocks of code. Two threads can concurrently execute the same synchronized method, provided that the method is invoked on different objects (that is, a.method() and b.method(), where a != b).

To demonstrate how monitors operate, let's rewrite the Counter example from the preceding section to take advantage of monitors, using the synchronized keyword:

public class Counter2 {
    private int count = 0;
    public synchronized int incr() {
        int n = count;
        count = n + 1;
        return n;
    }
}

Note that the incr() method has not been modified except for the addition of the synchronized keyword.

What would happen if this new Counter2 class were used in the scenario presented in Table 6.2 (the race condition)? The outcome of the same sequence of context switches is listed in Table 6.3.

Table 6.3. Counter scenario II, revised.

Thread 1 Thread 2 Count
cnt = counter.incr(); --- 0
(acquires the monitor) --- 0
n = count; // 0 --- 0
--- cnt = counter.incr(); 0
--- (can't acquire monitor) 0
count = n + 1; // 1 (blocked) 1
return n; // 0 (blocked) 1
(releases the monitor) (blocked) 1
--- (acquires the monitor) 1
--- n = count; // 1 1
--- count = n + 1; // 2 2
--- return n; // 1 2
--- (releases the monitor) 2


In Table 6.3, the sequence of operations begins the same as the scenario in Table 6.2. Thread 1 starts executing the incr() method of the Counter2 object but is interrupted by a context switch. In this example, however, when Thread 2 attempts to execute the incr() method on the same Counter2 object, the thread can't acquire the monitor and is blocked; the monitor is already owned by Thread 1. Thread 2 is suspended until the monitor becomes available. When Thread 1 releases the monitor, Thread 2 can then acquire the monitor and continue running.

The synchronized keyword is Java's solution to the concurrency control problem. As you saw in the Counter example, the potential race condition was eliminated by adding the synchronized modifier to the incr() method. All accesses to the incr() method of a counter were serialized by the addition of the synchronized keyword. Generally speaking, any method that modifies an object's attributes should be synchronized with the synchronized keyword.


NOTE: You may be wondering when you will see an actual monitor object. Anecdotal information has been presented about monitors, but you probably want to see some official documentation about what a monitor is and how you access it. Unfortunately, that is not possible. Java monitors have no official standing in the language specification, and their implementation is not directly visible to the programmer. Monitors are not Java objects--they have no attributes or methods. Monitors are a concept beneath Java's implementation of multithreading and concurrency. It is possible to access Java monitors at the native code level in the 1.x release of the Java virtual machine from Sun--the 1.1 Java Native Interface specification defines two methods that operate on monitors: MonitorEnter() and MonitorExit().

Non-synchronized Methods

Java monitors are used only in conjunction with synchronized methods. Methods that are not declared synchronized do not attempt to acquire ownership of an object's monitor before executing--they ignore monitors entirely. At any given moment, at most one thread can execute a synchronized method on an object, but an arbitrary number of threads can execute non-synchronized methods. This can lead to some surprising situations if you are not careful in deciding which methods should be synchronized. Consider the Account class in Listing 6.4.

Listing 6.4. The Account class.

class Account {
  private int balance;
  public Account(int balance) {
    this.balance = balance;
  }
  public synchronized void transfer(int amount, Account destination) {
    synchronized (destination) {
      this.withdraw(amount);
      Thread.yield();     // force a context switch
      destination.deposit(amount);
    }
  }
  public synchronized void withdraw(int amount) {
    if (amount > balance) {
      throw new RuntimeException("No overdraft protection!");
    }
    balance -= amount;
  }
  public synchronized void deposit(int amount) {
    balance += amount;
  }
  public int getBalance() {
    return balance;
  }
}

The attribute-modifying methods of the Account class are declared synchronized, but the getBalance() method is not synchronized. It appears that this class has no problem with race conditions--but it does!

To understand the race condition to which the Account class is subject, consider how a bank deals with accounts. To a bank, the correctness of its accounts is of the utmost importance--a bank that makes accounting errors or reports incorrect information would not have happy customers. To avoid reporting incorrect information, a bank would likely disable "inquiries" on an account while a transaction involving the account is in progress. This prevents the customer from viewing the result of a partially complete transaction. In Listing 6.4, the Account class's getBalance() method is not synchronized, and this can lead to problems.

Consider two Account objects and two different threads performing actions on these accounts. One thread is performing a balance transfer from one account to the other. The second thread is performing a balance inquiry. This code demonstrates the suggested activity:

public class XferTest implements Runnable {
  public static void main(String[] args) {
    XferTest xfer = new XferTest();
    xfer.a = new Account(100);
    xfer.b = new Account(100);
    xfer.amount = 50;
    Thread t = new Thread(xfer);
    t.start();
    Thread.yield();    // force a context switch
    System.out.println("Inquiry: Account a has : $" + xfer.a.getBalance());
    System.out.println("Inquiry: Account b has : $" + xfer.b.getBalance());
  }
  public Account a = null;
  public Account b = null;
  public int amount = 0;
  public void run() {
    System.out.println("Before xfer: a has : $" + a.getBalance());
    System.out.println("Before xfer: b has : $" + b.getBalance());
    a.transfer(amount, b);
    System.out.println("After xfer: a has : $" + a.getBalance());
    System.out.println("After xfer: b has : $" + b.getBalance());
  }
}


In this example, two Accounts are created, each with a $100 balance. A transfer is then initiated to move $50 from one account to the other. The transfer is not an operation that should affect the total balance of the two accounts; that is, the sum of the balance of the two accounts should remain constant at $200. If the balance inquiry is performed at just the right time, however, it is possible that the total amount of funds in these accounts could be incorrectly reported. If this program is run using the JDK version 1.0 for Solaris, the following output is printed:

Before xfer: a has : $100
Before xfer: b has : $100
Inquiry: Account a has : $50
Inquiry: Account b has : $100
After xfer: a has : $50
After xfer: b has : $150

The inquiry reports that the first account contains $50 and the second account contains $100. That's not $200! What happened to the other $50? Nothing has "happened" to the money--except that it is in the process of being transferred to the second account when the balance inquiry scans the accounts. Because the getBalance() method is not synchronized, a customer would have no problem executing an inquiry on accounts involved in the balance transfer. The lack of synchronization can leave some customer wondering why the accounts are $50 short.

If the getBalance() method is declared synchronized, the application has a different result. The modified code follows:

public synchronized int getBalance() {
    return balance;
  }

The balance inquiry is blocked until the balance transfer is complete. Here is the modified program's output:

Before xfer: a has : $100
Before xfer: b has : $100
Inquiry: Account a has : $50
Inquiry: Account b has : $150
After xfer: a has : $50
After xfer: b has : $150

Advanced Monitor Concepts

Monitors sound pretty simple. Add the synchronized modifier to your methods, and that's all there is to it. Well, not quite. Monitors themselves may be simple, but taken together with the rest of the programming environment, there are a few issues you should understand before you use synchronized methods. The following sections present a few tips and techniques you should master to become expert in concurrent Java programming.

static synchronized Methods

Methods that are declared synchronized attempt to acquire ownership of the target object's monitor. But what about static methods (methods that do not have an associated object)?

The language specification is fairly clear, if brief, about static synchronized methods. When a static synchronized method is called, the monitor acquired is said to be a per-class monitor--that is, there is one monitor for each class that regulates access to all static methods of that class. Only one static synchronized method in a class can be active at a given moment.

The synchronized Statement

It is not possible to use synchronized methods on some types of objects. For example, it is not possible to add any methods to Java array objects (much less synchronized methods). To get around this restriction, Java has a second way of interacting with an object's monitor. The synchronized statement is defined to have the following syntax:

synchronized ( Expression ) Statement

Executing a synchronized statement has the same effect as calling a synchronized method--ownership of an object's monitor is acquired before a block of code can be executed. With the synchronized statement, the object whose monitor is up for grabs is the object resulting from Expression (which must be an object type, not an elemental type like int, double, and so on).

One of the most important uses of the synchronized statement involves controlling access to array objects. The following example demonstrates how to use the synchronized statement to provide thread-safe access to an array:

void safe_lshift(byte[] array, int count) {
    synchronized(array) {
        System.arraycopy(array, count, array, 0, array.size - count);
    }
}

Before modifying the array in this example, the virtual machine assigns ownership of array's monitor to the currently executing thread. Other threads trying to acquire array's monitor are forced to wait until the array-copy operation is complete. Of course, accesses to the array that are not guarded by a synchronized statement are not blocked; so be careful!

The synchronized statement is also useful when modifying an object's public variables directly. Here's an example:

void call_method(SomeClass obj) {
    synchronized(obj) {
        obj.variable = 5;
    }

}

PUBLIC OR NOT?
There is debate within the Java community about the potential danger of declaring attributes to be public. When concurrency is considered, it becomes apparent that public attributes can lead to thread-unsafe code. Here's why: public attributes can be accessed by any thread without the benefit of protection by a synchronized method. When you declare an attribute public, you relinquish control over updates to that attribute; any programmer using your code has a license to access (and update) public attributes directly.

In general, it is not a good idea to declare non-final attributes to be public. Not only can doing so introduce thread-safety problems, it can make your code difficult to modify and support in later revisions.

Note, however, that Java programmers frequently define immutable symbolic constants as public final class attributes (such as Event.ACTION_EVENT). Attributes declared this way do not have thread-safety issues. (Race conditions involve only objects whose values can be modified.)


When Not to Be synchronized

By now, you should be able to write thread-safe code using the synchronized keyword. When should you really use the synchronized keyword? Are there situations in which you should not use synchronized? Are there drawbacks to using synchronized?

The most common reason developers don't use synchronized is that they write single-threaded, single-purpose code. For example, CPU-bound tasks do not benefit much from multithreading. A compiler does not perform much better if it is threaded. The Java compiler from Sun does not contain many synchronized methods. For the most part, it assumes that it is executing in its own thread of control, without having to share its resources with other threads.

Another common reason for avoiding synchronized methods is that they do not perform as well as non-synchronized methods. In simple tests in the JDK version 1.0.1 from Sun, synchronized methods have been shown to be three to four times slower than their non- synchronized counterparts. Although this doesn't mean your entire application will be three or four times slower, it is a performance issue nonetheless. Some programs demand that every ounce of performance be squeezed out of the runtime system. In this situation, it may be appropriate to avoid the performance overhead associated with synchronized methods.

Java 1.1 Inner Classes

With the release of Java 1.1, Java now supports inner classes (refer to Chapter 5, "Classes, Packages, and Interfaces," for an introduction to inner classes). Inner classes are classes declared within another class. An instance of an inner class has a special relationship to an instance of the outer class--referred to as the enclosing instance. Inner-class instances are permitted to access all the fields of the enclosing instance, including private fields. Additionally, an inner-class instance can modify the variables of its enclosing instance.

Inner classes introduce potential problems for programs that use threads. Consider the following example of an inner class and an enclosing class:

public class Enclosing {
  private int privateVar = 1;
  public synchronized void update() {
    privateVar = privateVar + 1;
  }
  public class Inner {
    public synchronized void update() {
      privateVar = privateVar + 1;
    }
  }
}

The Enclosing class has three fields: a private variable, a synchronized method, and the Inner class. This example demonstrates that the private variables of an enclosing instance can be updated from methods of the enclosing instance and the inner instance--both Enclosing and Inner have an update() method that modifies privateVar.

Calling a synchronized method from the enclosing instance is a safe way to update the enclosing instance:

Enclosing enclosingInstance = getEnclosingInstance();
enclosingInstance.update(); // SAFE

However, it is not particularly safe to call a synchronized method from an inner instance to update the enclosing instance:

Enclosing enclosingInstance = getEnclosingInstance();
Enclosing.Inner innerInstance = enclosingInstance.new Inner();
innerInstance.update(); // UNSAFE

This second code example is unsafe because the use of inner classes involves more than one object--and thus, more than one monitor. When the code innerInstance.update() is called, the only monitor acquired is that of the innerInstance. The monitor of the enclosingInstance is never acquired, even though its privateVar is updated. To make the code thread safe, the Inner class's update() method should acquire the monitor of its enclosing instance, like this:

class Inner {
  public synchronized void update() {
    synchronized (Enclosing.this) {      // must also lock the enclosing instance
      privateVar = privateVar + 1;
    }
  }
}

In general, inner classes that access variables from an enclosing instance should use the synchronized statement to lock the enclosing instance while its variables are being accessed. The generalized form for this type of lock follows:

synchronized (name-of-enclosing-class . this) {
// your code here
}

Deadlocks

Sometimes referred to as a deadly embrace, a deadlock is one of the worst situations that can happen in a multithreaded environment. Java programs are not immune to deadlocks, and programmers must take care to avoid them.

A deadlock is a situation that causes two or more threads to hang, that is, to be unable to proceed. In the simplest case, two threads are each trying to acquire a monitor that is already owned by the other thread. Each thread goes to sleep, waiting for the desired monitor to become available--but the monitors never become available. (The first thread waits for the monitor owned by the second thread, and the second thread waits for the monitor owned by the first thread. Because each thread is waiting for the other, it never releases its monitor to the other thread.)

The sample application in Listing 6.5 should give you an understanding of how a deadlock happens.

Listing 6.5. A deadlock.

public class Deadlock implements Runnable {
  public static void main(String[] args) {
        Deadlock d1 = new Deadlock();
        Deadlock d2 = new Deadlock();
        Thread t1 = new Thread(d1);
        Thread t2 = new Thread(d2);
        d1.grabIt = d2;
        d2.grabIt = d1;
        t1.start();
        t2.start();
        try { t1.join(); t2.join(); } catch(InterruptedException e) { }
        System.exit(0);
  }
  Deadlock grabIt;
  public synchronized void run() {
        try { Thread.sleep(2000); } catch(InterruptedException e) { }
        grabIt.sync_method();
  }
  public synchronized void sync_method() {
        try { Thread.sleep(2000); } catch(InterruptedException e) { }
        System.out.println("in sync_method");
  }
}

In this class, main() launches two threads, each of which invokes the synchronized run() method on a Deadlock object. When the first thread wakes up, it attempts to call the sync_method() of the other Deadlock object. Obviously, the second Deadlock's monitor is owned by the second thread, so the first thread begins waiting for the monitor. When the second thread wakes up, it tries to call the sync_method() of the first Deadlock object. Because that Deadlock's monitor is already owned by the first thread, the second thread begins waiting. Because the threads are waiting for each other, neither will ever wake up.


NOTE: If you run the deadlock application shown in Listing 6.5, you will notice that it never exits. That is understandable; after all, that is what a deadlock is. How can you tell what is really going on inside the virtual machine? There is a trick you can use with the Solaris/UNIX JDK to display the status of all threads and monitors: Press Ctrl+\ in the terminal window in which the Java application is running. This action sends the virtual machine a signal to dump the state of the VM. Here is a partial listing of the monitor table dumped several seconds after launching the deadlock application:
Deadlock@EE300840/EE334C20 (key=0xee300840):     monitor owner: "Thread-5"
    Waiting to enter:
        "Thread-4"
Deadlock@EE300838/EE334C18 (key=0xee300838):     monitor owner: "Thread-4"
    Waiting to enter:
      "Thread-5"


Numerous algorithms are available for preventing and detecting deadlock situations, but those algorithms are beyond the scope of this chapter (many database and operating system texts cover deadlock-detection algorithms in detail). Unfortunately, the Java virtual machine itself does not perform any deadlock detection or notification. There is nothing that prevents the virtual machine from doing so, however, so this behavior may be added to future versions of the virtual machine.

Using volatile

It is worth mentioning that the volatile keyword is supported as a variable modifier in Java. The language specification states that the volatile qualifier instructs the compiler to generate loads and stores on each access to an attribute, rather than caching the value in a register. The intent of the volatile keyword is to provide thread-safe access to an attribute, but volatile falls short of this goal.

In the 1.x JDK virtual machine, the volatile keyword is ignored. It is unclear whether volatile has been abandoned in favor of monitors and synchronized methods or whether the keyword was included solely for a C and C++ look and feel. Regardless, volatile is useless--use synchronized methods rather than the volatile keyword.

Synchronization

After learning how synchronized methods are used to make Java programs thread safe, you may wonder what the big deal is about monitors. They are just object locks, right? Not true! Monitors are more than locks; monitors are also used to coordinate multiple threads by using the wait() and notify() methods available in every Java object.

The Need for Thread Coordination

In a Java program, threads are often interdependent--one thread can depend on another thread to complete an operation or to service a request. For example, a spreadsheet program can run an extensive recalculation as a separate thread. If a user-interface (UI) thread attempts to update the spreadsheet's display, the UI thread should coordinate with the recalculation thread, starting the screen update only when the recalculation thread has successfully completed.

There are many other situations in which it is useful to coordinate two or more threads. The following list identifies only some of the possibilities:

  • Shared buffers are often used to communicate data between threads. In this scenario, one thread writes to a shared buffer (the writer) and one thread reads from the buffer (the reader). When the reader thread attempts to read from the buffer, it should coordinate with the writer thread, retrieving data from the shared buffer only after the writer thread has put it there. If the buffer is empty, the reader thread should wait for the data. The writer thread notifies the reader thread when it has completed filling the buffer so that the reader can continue.

  • Many threads may have to perform an identical action, such as loading an image file across the network. These threads can reduce the overall system load if only one thread performs the work while the other threads wait for the work to be completed. (The waiting threads must wait without consuming CPU time by temporarily transitioning into the NOT RUNNABLE thread state; this is possible, and is discussed later in this chapter.) This is precisely the model used in the java.awt.MediaTracker class.

It is no accident that the previous examples repeatedly use the words wait and notify. These words express the two concepts central to thread coordination: A thread waits for some condition or event to occur, and you notify a waiting thread that a condition or event has occurred. The words wait and notify are also used in Java as the names of the methods you call to coordinate threads: wait() and notify(), in class Object.

As noted in "Monitors," earlier in this chapter, every Java object has an associated monitor. That fact turns out to be useful at this point because monitors are also used to implement Java's thread-coordination primitives. Although monitors are not directly visible to the programmer, an API is provided in class Object that enables you to interact with an object's monitor. This API consists of two methods: wait() and notify().

Conditions, wait(), and notify()

Threads are usually coordinated using a concept known as a condition, or a condition variable. A condition is a logical statement that must hold true in order for a thread to proceed; if the condition does not hold true, the thread must wait for the condition to become true before continuing. In Java, this pattern is usually expressed as follows:

while ( ! the_condition_I_am_waiting_for ) {
    wait();
}

First, check to see whether the desired condition is already true. If it is true, there is no need to wait. If the condition is not yet true, call the wait() method. When wait() ends, recheck the condition to make sure that it is now true.

Invoking wait() on an object pauses the current thread and adds the thread to the condition variable wait queue of the object's monitor. This queue contains a list of all the threads that are currently blocked inside wait() on that object. The thread is not removed from the wait queue until notify() is invoked on that object from a different thread. A call to notify() wakes a single waiting thread, notifying the thread that a condition of the object has changed.

There are two additional varieties of the wait() method. The first version takes a single parameter--a timeout value in milliseconds. The second version has two parameters--a more precise timeout value, specified in milliseconds and nanoseconds. These methods are used when you do not want to wait indefinitely for an event. If you want to abandon the wait after a fixed period of time (referred to as timing out), you should use either of the following methods:

  • wait(long milliseconds);

  • wait(long milliseconds, int nanoseconds);

Unfortunately, these methods do not provide a way to determine how the wait() was ended--whether a notify() occurred or whether the method timed out. This is not a big problem, however, because you can recheck the wait condition and the system time to determine which event has occurred.


CAUTION: In the JDK versions 1.0.x and 1.1, the wait(int millisecond, int nanosecond) method uses the nanosecond parameter to round the millisecond parameter to the nearest millisecond. Waiting is not yet supported in nanosecond granularity.

The wait() and notify() methods must be invoked from within a synchronized method or from within a synchronized statement. This requirement is discussed in further detail in "Monitor Ownership," later in this chapter.

A Thread Coordination Example

A classic example of thread coordination used in many computer science texts is the bounded buffer problem. This problem involves using a fixed-size memory buffer to communicate between two processes or threads. To solve this problem, you must coordinate the reader and writer threads so that the following are true:

  • When the writer thread attempts to write to a full buffer, the writer is suspended until some items are removed from the buffer.

  • When the reader thread removes items from the full buffer, the writer thread is notified of the buffer's changed condition and may continue writing.

  • When the reader thread attempts to read from an empty buffer, the reader is suspended until some items are added to the buffer.

  • When the writer adds items to the empty buffer, the reader thread is notified of the buffer's changed condition and may continue reading.

The following class listings demonstrate a Java implementation of the bounded buffer problem. There are three main classes in this example: the Producer, the Consumer, and the Buffer. Let's start with the Producer:

public class Producer implements Runnable {
  private Buffer buffer;
  public Producer(Buffer b) {
        buffer = b;
  }
  public void run() {
      for (int i=0; i<250; i++) {
          buffer.put((char)('A' + (i%26)));   // write to the buffer
      }
  }
}

The Producer class implements the Runnable interface (which should give you a hint that it will be used in a Thread). When the Producer's run() method is invoked, 250 characters are written in rapid succession to a buffer.

The Consumer class is as simple as the Producer:

public class Consumer implements Runnable {
  private Buffer buffer;
  public Consumer(Buffer b) {
        buffer = b;
  }
  public void run() {
        for (int i=0; i<250; i++) {
          System.out.println(buffer.get());    // read from the buffer
        }
  }
}

The Consumer is also a Runnable interface. Its run() method greedily reads 250 characters from a buffer.

The Buffer class has been mentioned already, including two of its methods: put(char) and get(). Listing 6.6 shows the Buffer class in its entirety.

Listing 6.6. The Buffer class.

public class Buffer {
  private char[] buf;   // buffer storage
  private int last;     // last occupied position
  public Buffer(int sz) {
        buf = new char[sz];
        last = 0;
  }
  public boolean isFull()  { return (last == buf.length); }
  public boolean isEmpty() { return (last == 0);          }
  public synchronized void put(char c) {
        while(isFull()) {                    // wait for room to put stuff
          try { wait(); } catch(InterruptedException e) { }
        }
        buf[last++] = c;
        notify();
  }
  public synchronized char get() {
        while(isEmpty()) {                   // wait for stuff to read
          try { wait(); } catch(InterruptedException e) { }
        }
        char c =  buf[0];
        System.arraycopy(buf, 1, buf, 0, --last);
        notify();
        return c;
  }
}

NOTE: When you first begin using wait() and notify(), you may notice a contradiction. The wait() and notify() methods must be called from synchronized methods, so if wait() is called inside a synchronized method, how can a different thread enter a synchronized method in order to call notify()? Doesn't the waiting thread own the object's monitor, preventing other threads from entering the synchronized method?

The answer to this paradox is that wait() temporarily releases ownership of the object's monitor; before wait() can return, however, it must reacquire ownership of the monitor. By releasing the monitor, the wait() method allows other threads to acquire the monitor (which gives them the ability to call notify()).

The Buffer class is just that--a storage buffer. You can use put() to put items into the buffer (in this case, characters), and you can use get() to get items out of the buffer.

Note the use of wait() and notify() in these methods. In the put() method, a wait() is performed while the buffer is full; no more items can be added to the buffer while it is full. At the end of the get() method, the call to notify() ensures that any thread waiting in the put() method will be activated and allowed to continue adding an item to the buffer. Similarly, a wait() is performed in the get() method if the buffer is empty; no items can be removed from an empty buffer. The put() method calls notify() to ensure that any thread waiting in get() will be wakened.


NOTE: Java provides two classes similar to the Buffer class presented in this example. These classes, java.io.PipedOutputStream and java.io.PipedInputStream, are useful in communicating streams of data between threads. If you unpack the src.zip file shipped with the JDK, you can examine these classes to see how they handle interthread coordi-nation.

Advanced Thread Coordination

The wait() and notify() methods simplify the task of coordinating multiple threads in a concurrent Java program. However, to make full use of these methods, you should understand a few additional details. The following sections present more material about thread coordination in Java.

Monitor Ownership

The wait() and notify() methods have one major restriction you must observe: You can call these methods only when the current thread owns the monitor of the object. Most frequently, wait() and notify() are invoked from within a synchronized method, as in the following example:

public synchronized void method() {
    ...
    while (!condition) {
      wait();
    }
    ...
}

In this case, the synchronized modifier guarantees that the thread invoking the wait() call already owns the monitor when it calls wait().

If you attempt to call wait() or notify() without first acquiring ownership of the object's monitor (for example, from a non-synchronized method), the virtual machine throws an IllegalMonitorStateException. The following example demonstrates what happens when you call wait() without first acquiring ownership of the monitor:

public class NonOwnerTest {
  public static void main(String[] args) {
        NonOwnerTest not = new NonOwnerTest();
        not.method();
  }
  public void method() {
        try { wait(); } catch(InterruptedException e) { }   // a bad thing to do!
  }
}

If you run this Java application, the following text is printed to the terminal:

java.lang.IllegalMonitorStateException: current thread not owner
        at java.lang.Object.wait(Object.java)
        at NonOwnerTest.method(NonOwnerTest.java:10)
        at NonOwnerTest.main(NonOwnerTest.java:5)

When you invoke the wait() method on an object, you must own the object's monitor if you are to avoid this exception.


MONITORS AND THE synchronized STATEMENT:

All Java objects can participate in thread synchronization by using the wait() and notify() methods. However, the "monitor ownership" requirement introduces a quirk for some object types, such as arrays. (Strangely enough, Java array types inherit from the java.lang.Object class, where the wait() and notify() methods are defined.) The wait() and notify() methods can be called on Java array objects, but monitor ownership must be established using the synchronized statement rather than a synchronized method. The following code demonstrates monitor usage as applied to a Java array:
// wait for an event on this array
Object[] array = getArray();
synchronized (array) {
array.wait();
}
...
// notify waiting threads
Object[] array = getArray();
synchronized (array) {
array.notify();
}


Multiple Waiters

It is possible for multiple threads to be waiting on the same object. This can happen when multiple threads wait for the same event. For example, recall the Buffer class described earlier; the buffer was operated on by a single Producer and a single Consumer. What would happen if there were multiple Producers? If the buffer filled, different Producers might attempt to use put() to place items into the buffer; they would all block inside the put() method, waiting for a Consumer to come along and free up space in the buffer.

When you call notify(), there may be zero, one, or more threads blocked in a wait() on the monitor. If there are no threads waiting, the call to notify() is a no-op--it does not affect any other threads. If there is a single thread in wait(), that thread is notified and begins waiting for the monitor to be released by the thread that called notify(). If two or more threads are in a wait(), the virtual machine picks a single waiting thread and notifies that thread. (The method used to "pick" a waiting thread varies from platform to platform--your programs should not rely on the VM to select a specific thread from the pool of waiting threads.)

Using notifyAll()

In some situations, you may want to notify every thread currently waiting on an object. The Object API provides a method to do this: notifyAll(). The notify() method wakes only a single waiting thread, but the notifyAll() method wakes every thread currently waiting on the object.

When would you want to use notifyAll()? Consider the java.awt.MediaTracker class. This class is used to track the status of images being loaded over the network. Multiple threads may wait on the same MediaTracker object, waiting for all the images to be loaded. When the MediaTracker detects that all images have been loaded, notifyAll() is called to inform every waiting thread that the images have been loaded. notifyAll() is used because the MediaTracker does not know how many threads are waiting; if notify() were used, some of the waiting threads may not receive notification that the transfer was completed. These threads would continue waiting, probably hanging the entire applet.

Listing 6.6, earlier in this chapter, can also benefit from the use of notifyAll(). In that code, the Buffer class used the notify() method to send a notification to a single thread waiting on an empty or a full buffer. However, there was no guarantee that only a single thread was waiting; multiple threads may have been waiting for the same condition. Listing 6.7 shows a modified version of the Buffer class (named Buffer2) that uses notifyAll().

Listing 6.7. The Buffer2 class, using notifyAll().

public class Buffer2 {
  private char[] buf;               // storage
  private int last = 0;             // last occupied position
  private int writers_waiting = 0;  // # of threads waiting in put()
  private int readers_waiting = 0;  // # of threads waiting in get()
  public Buffer2(int sz) {
        buf = new char[sz];
}
  public boolean isFull()  { return (last == buf.length); }
  public boolean isEmpty() { return (last == 0);          }
  public synchronized void put(char c) {
        while(isFull()) {
          try     { writers_waiting++;  wait(); }
          catch   (InterruptedException e) { }
          finally { writers_waiting--; }
        }
        buf[last++] = c;
        if (readers_waiting > 0) {
          notifyAll();
        }
  }
  public synchronized char get() {
        while(isEmpty()) {
          try     { readers_waiting++;  wait(); }
          catch   (InterruptedException e) { }
          finally { readers_waiting--; }
        }
        char c =  buf[0];
        System.arraycopy(buf, 1, buf, 0, --last);
        if (writers_waiting > 0) {
          notifyAll();
        }
        return c;
  }
}

The get() and put() methods have been made more intelligent. They now check to see whether any notification is necessary and then use notifyAll() to broadcast an event to all waiting threads.

Summary

This chapter was a whirlwind tour of multithreaded programming in Java. Among other things, the chapter covered the following:

  • Creating your own thread classes by subclassing Thread or implementing Runnable

  • Using the ThreadGroup class to manage groups of threads

  • Understanding thread states and thread scheduling

  • Making your classes thread-safe by using the synchronized keyword to protect objects from concurrent modification

  • Understanding how monitors affect concurrent programming in Java

  • Coordinating the actions of multiple threads by calling the wait() and notify() methods

Java threads are not difficult to use. After reading this chapter, you should begin to see how threads can be used to improve your everyday Java programming.

TOCBACKFORWARDHOME


©Copyright, Macmillan Computer Publishing. All rights reserved.