This chapter introduces you to multithreaded programs and how multithreading is supported in Java. You'll learn how to create, run, and synchronize multiple threads in your programs. You'll also learn about thread scheduling and how a thread's priority determines when it is scheduled. When you finish this chapter you will be able to develop multithreaded programs using Java.
All the sample programs you developed in the preceding chapters have had only a single thread of execution. Each program proceeded sequentially, one instruction after another, until it completed its processing and terminated.
Multithreaded programs are similar to the single-threaded programs that you have been studying. They differ only in the fact that they support more than one concurrent thread of execution-that is, they are able to simultaneously execute multiple sequences of instructions. Each instruction sequence has its own unique flow of control that is independent of all others. These independently executed instruction sequences are known as threads.
If your computer has only a single CPU, you might be wondering how it can execute more than one thread at the same time. In single-processor systems, only a single thread of execution occurs at a given instant. The CPU quickly switches back and forth between several threads to create the illusion that the threads are executing at the same time. Single-processor systems support logical concurrency, not physical concurrency. Logical concurrency is the characteristic exhibited when multiple threads execute with separate, independent flows of control. On multiprocessor systems, several threads do, in fact, execute at the same time, and physical concurrency is achieved. The important feature of multithreaded programs is that they support logical concurrency, not whether physical concurrency is actually achieved.
Many programming languages support multiprogramming. Multiprogramming is the logically concurrent execution of multiple programs. For example, a program can request that the operating system execute programs A, B, and C by having it spawn a separate process for each program. These programs can run in parallel, depending upon the multiprogramming features supported by the underlying operating system. Multithreading differs from multiprogramming in that multithreading provides concurrency within the context of a single process and multiprogramming provides concurrency between processes. Threads are not complete processes in and of themselves. They are a separate flow of control that occurs within a process. Figure 8.1 illustrates the difference between multithreading and multiprogramming.
Figure 8.1 : Multithreading versus multiprogramming.
An executing program is generally associated with a single process. The advantage of multithreading is that concurrency can be used within a process to provide multiple simultaneous services to the user. Multithreading also requires less processing overhead than multiprogramming because concurrent threads are able to share common resources more easily. Multiple executing programs tend to duplicate resources and share data as the result of more time-consuming interprocess communication.
Java provides extensive support for both multithreading and multiprogramming. Multithreading is covered in this chapter. Java's support for multiprogramming is covered in Chapter 12, "Portable Software and the java.lang Package."
Java's multithreading support is centered around the java.lang.Thread class. The Thread class provides the capability to create objects of class Thread, each with its own separate flow of control. The Thread class encapsulates the data and methods associated with separate threads of execution and allows multithreading to be integrated within the object-oriented framework.
Java provides two approaches to creating threads. In the first
approach, you create a subclass of class Thread and override
the run() method to provide an entry point into the thread's
execution. When you create an instance of your Thread
subclass, you invoke its start() method to cause the
thread to execute as an independent sequence of instructions.
The start() method is inherited from the Thread
class. It initializes the Thread object using your operating
system's multithreading capabilities and invokes the run()
method. You learn how to create threads using this approach in
the next section.
Note |
This chapter makes heavy use of the Java API methods defined for class Thread and related classes. If you haven't obtained and installed a copy of the Java API documentation, now is a good time to do so. |
The approach to creating threads identified in the previous paragraph is very simple and straightforward. However, it has the drawback of requiring your Thread objects to be under the Thread class in the class hierarchy. In some cases, as you'll see when you study applets in Part VI, "Programming the Web with Applets and Scripts," this requirement can be somewhat limiting.
Java's other approach to creating threads does not limit the location of your Thread objects within the class hierarchy. In this approach, your class implements the java.lang.Runnable interface. The Runnable interface consists of a single method, the run() method, which must be overridden by your class. The run() method provides an entry point into your thread's execution. In order to run an object of your class as an independent thread, you pass it as an argument to a constructor of class Thread. You learn how to create threads using this approach later in this chapter in the section titled "Implementing Runnable."
In this section, you create your first multithreaded program by creating a subclass of Thread and then creating, initializing, and starting two Thread objects from your class. The threads will execute concurrently and display Java is hot, aromatic, and invigorating. to the console window.
Create a ch08 directory under c:\java\jdg and enter the source code from Listing 8.1 into ThreadTest1.java. Then compile it using the command javac ThreadTest1.java.
Listing 8.1. The source code of the ThreadTest1 program.import java.lang.Thread;
import java.lang.System;
import java.lang.Math;
import java.lang.InterruptedException;
class ThreadTest1 {
public static void main(String args[]) {
MyThread thread1 = new MyThread("thread1: ");
MyThread thread2 = new MyThread("thread2: ");
thread1.start();
thread2.start();
boolean thread1IsAlive = true;
boolean thread2IsAlive = true;
do {
if(thread1IsAlive && !thread1.isAlive()){
thread1IsAlive = false;
System.out.println("Thread 1 is dead.");
}
if(thread2IsAlive && !thread2.isAlive()){
thread2IsAlive = false;
System.out.println("Thread 2 is dead.");
}
}while(thread1IsAlive || thread2IsAlive);
}
}
class MyThread extends Thread {
static String message[] = {"Java","is","hot,","aromatic,","and",
"invigorating."};
public MyThread(String id) {
super(id);
}
public void run() {
String name = getName();
for(int i=0;i<message.length;++i) {
randomWait();
System.out.println(name+message[i]);
}
}
void randomWait(){
try {
sleep((long)(3000*Math.random()));
}catch (InterruptedException x){
System.out.println("Interrupted!");
}
}
}
This program creates two threads of execution, thread1
and thread2, from the MyThread class. It then
starts both threads and executes a do statement that
waits for the threads to die. The threads display the Java
is hot, aromatic, and invigorating. message word by word,
while waiting a short, random amount of time between each word.
Because both threads share the console window, the program's output
identifies which threads were able to write to the console at
various times during the program's execution.
Note |
The Java documentation refers to threads that have completed their execution as being dead. The term is descriptive, but somewhat morose. |
Run ThreadTest1 to get an idea of the output that it produces. Each time you run the program you might get a different program display. This is because the program uses a random number generator to determine how long each thread should wait before displaying its output. Look at the following output:
C:\java\jdg\ch08>java ThreadTest1
thread1: Java
thread2: Java
thread2: is
thread2: hot,
thread2: aromatic,
thread1: is
thread1: hot,
thread2: and
thread1: aromatic,
thread1: and
thread2: invigorating.
Thread 2 is dead.
thread1: invigorating.
Thread 1 is dead.
This output shows that thread1 executed first and displayed Java to the console window. It then waited to execute while thread2 displayed Java, is, hot,, and aromatic,. After that, thread2 waited while thread1 continued its execution. thread1 displayed is and then hot,. At this point, thread2 took over again. thread2 displayed and and then went back into waiting. thread1 then displayed aromatic, and and. thread2 finished its execution by displaying invigorating.. Having completed its execution, thread2 died, leaving thread1 as the only executing task. thread1 displayed invigorating. and then completed its execution.
The ThreadTest1 class consists of a single main() method. This method begins by creating thread1 and thread2 as new objects of class MyThread. It then starts both threads using the start() method. At this point, main() enters a do loop that continues until both thread1 and thread2 are no longer alive. The loop monitors the execution of the two threads and displays a message when it has detected the death of each thread. It uses the isAlive() method of the Thread class to tell when a thread has died. The thread1IsAlive and thread2IsAlive variables are used to ensure that a thread's obituary is only displayed once.
The MyThread class extends class Thread. It declares a statically initialized array, named message[], that contains the message to be displayed by each thread. It has a single constructor that invokes the Thread class constructor via super(). It contains two access methods: run() and randomWait(). The run() method is required. It uses the getName() method of class Thread to get the name of the currently executing thread. It then prints each word of the output display message while waiting a random length of time between each print. The randomWait() method invokes the sleep() method within a try statement. The sleep() method is another method inherited from class Thread. It causes the currently executing task to "go to sleep" or wait until a randomly specified number of milliseconds has transpired. Because the sleep() method throws the InterruptedException when its sleep is interrupted (how grouchy!), the exception is caught and handled by the randomWait() method. The exception is handled by displaying the fact that an interruption has occurred to the console window.
In the previous section, you created a multithreaded program by creating the MyThread subclass of Thread. In this section, you create a program with similar behavior, but you create your threads as objects of the class MyClass, which is not a subclass of Thread. MyClass will implement the Runnable interface and objects of MyClass will be executed as threads by passing them as arguments to the Thread constructor.
The ThreadTest2 program's source code is shown in Listing 8.2. Enter it into the ThreadTest2.java file and compile it.
Listing 8.2. The source code of the ThreadTest2 program.import java.lang.Thread;
import java.lang.System;
import java.lang.Math;
import java.lang.InterruptedException;
import java.lang.Runnable;
class ThreadTest2 {
public static void main(String args[]) {
Thread thread1 = new Thread(new MyClass("thread1: "));
Thread thread2 = new Thread(new MyClass("thread2: "));
thread1.start();
thread2.start();
boolean thread1IsAlive = true;
boolean thread2IsAlive = true;
do {
if(thread1IsAlive && !thread1.isAlive()){
thread1IsAlive = false;
System.out.println("Thread 1 is dead.");
}
if(thread2IsAlive && !thread2.isAlive()){
thread2IsAlive = false;
System.out.println("Thread 2 is dead.");
}
}while(thread1IsAlive || thread2IsAlive);
}
}
class MyClass implements Runnable {
static String message[] = {"Java","is","hot,","aromatic,","and",
"invigorating."};
String name;
public MyClass(String id) {
name = id;
}
public void run() {
for(int i=0;i<message.length;++i) {
randomWait();
System.out.println(name+message[i]);
}
}
void randomWait(){
try {
Thread.currentThread().sleep((long)(3000*Math.random()));
}catch (InterruptedException x){
System.out.println("Interrupted!");
}
}
}
The ThreadTest2 program is very similar to ThreadTest1. It differs only in the way that the threads are created. You should run ThreadTest2 a few times to examine its output. Here are the results of a sample run I made on my computer:
C:\java\jdg\ch08>java ThreadTest2
thread2: Java
thread1: Java
thread2: is
thread2: hot,
thread1: is
thread2: aromatic,
thread1: hot,
thread1: aromatic,
thread1: and
thread2: and
thread1: invigorating.
Thread 1 is dead.
thread2: invigorating.
Thread 2 is dead.
These results show thread2 beginning its output before thread1. It does not mean that thread2 began executing before thread1. Thread1 executed first, but went to sleep before generating any output. Thread2 then executed and started its output display before going to sleep. You can follow these results on your own to analyze how thread1 and thread2 switched back and forth during their execution to display their results to the console window.
The main() method of ThreadTest2 differs from that of ThreadTest1 in the way that it creates thread1 and thread2. ThreadTest1 created the threads as new instances of the MyThread class. ThreadTest2 was not able to create the threads directly, because MyClass is not a subclass of Thread. Instead, ThreadTest2 first created instances of MyClass and then passed them to the Thread() constructor, creating instances of class Thread. The Thread() constructor used by ThreadTest2 takes as its argument any class that implements the Runnable interface. This is an example of the flexibility and multiple-inheritance features provided by Java interfaces. The rest of the ThreadTest2 main() method is the same as that of ThreadTest1.
MyClass is declared as implementing the Runnable interface. This is a simple interface to implement; it only requires that you implement the run() method. MyClass declares the name variable to hold the names of MyClass objects that are created. In the first example, the MyThread class did not need to do this because a thread-naming capability was provided by Thread and inherited by MyThread. MyClass contains a simple constructor that initializes the name variable.
The run() methods of ThreadTest2 and ThreadTest1 are nearly identical, differing only with respect to the name issue. This is also true of the randomWait() method. In ThreadTest2, the randomWait() method must use the currentThread() method of class Thread to acquire a reference to an instance of the current thread in order to invoke its sleep() method.
Because these two examples are so similar, you might be wondering why you would pick one approach to creating a class over another. The advantage of using the Runnable interface is that your class does not need to extend the Thread class. This will be very helpful feature when you start using multithreading in applets in Part VI of this book. The only disadvantages to this approach are ones of convenience. You have to do a little more work to create your threads and to access their methods.
You have now learned how to declare, create, initialize, start, and run Java threads. The ThreadTest1 and ThreadTest2 programs also introduced you to the concept of a thread's death. Threads transition through several states from the time they are created until the time of their death. This section reviews these states.
A thread is created by creating a new object of class Thread or of one of its subclasses. When a thread is first created, it does not exist as an independently executing set of instructions. Instead, it is a template from which an executing thread will be created. It first executes as a thread when it is started using the start() method and run via the run() method. Before a thread is started it is said to be in the new thread state. After a thread is started, it is in the runnable state. When a class is in the runnable state, it may be executing or temporarily waiting to share processing resources with other threads. A runnable thread enters an extended wait state when one of its methods is invoked that causes it to drop from the runnable state into a not runnable state. In the not runnable state, a thread is not just waiting for its share of processing resources, but is blocked waiting for the occurrence of an event that will send it back to the runnable state.
For example, the sleep() method was invoked in the ThreadTest1
and ThreadTest2 programs to cause a thread to wait for
a short period of time so that the other thread could execute.
The sleep() method causes a thread to enter the not runnable
state until the specified time has expired. A thread may also
enter the not runnable state while it is waiting for I/O to be
completed, or as the result of the invocation of other methods.
Chapter 12 provides a detailed description
of the methods of the Thread class that are inherited
by all threads.
Note |
There is no connection between a thread's runnable state and a class's Runnable interface. |
A thread leaves the not runnable state and returns to the runnable state when the event that it is waiting for has occurred. For example, a sleeping thread must wait for its specified sleep time to occur. A thread that is waiting on I/O must wait for the I/O operation to be completed.
A thread may transition from the new thread, runnable, or not runnable state to the dead state when its stop() method is invoked or the thread's execution is completed. When a thread enters the dead state, it's a goner. It can't be revived and returned to any other state.
From an abstract or a logical perspective, multiple threads execute as concurrent sequences of instructions. This may be physically true for multiprocessor systems, under certain conditions. However, in the general case, multiple threads do not always physically execute at the same time. Instead, the threads share execution time with each other based on the availability of the system's CPU (or CPUs).
The approach used to determining which threads should execute at a given time is referred to as scheduling. Scheduling is performed by the Java runtime system. It schedules threads based on their priority. The highest-priority thread that is in the runnable state is the thread that is run at any given instant. The highest-priority thread continues to run until it enters the death state, enters the not runnable state, or has its priority lowered, or when a higher-priority thread becomes runnable.
A thread's priority is an integer value between MIN_PRIORITY and MAX_PRIORITY. These constants are defined in the Thread class. In Java 1.0, MIN_PRIORITY is 1 and MAX_PRIORITY is 10. A thread's priority is set when it is created. It is set to the same priority as the thread that created it. The default priority of a thread is NORM_PRIORITY and is equal to 5. The priority of a thread can be changed using the setPriority() method.
Java's approach to scheduling is referred to as preemptive scheduling. When a thread of higher priority becomes runnable, it preempts threads of lower priority and is immediately executed in their place. If two or more higher-priority threads become runnable, the Java scheduler alternates between them when allocating execution time.
There are many situations in which multiple threads must share access to common objects. For example, all of the programs in this chapter have illustrated the effects of multithreading by having multiple executing threads write to the Java console, a common shared object. These examples have not required any coordination or synchronization in the way the threads access the console window: Whatever thread was currently executing was able to write to the console window. No coordination between concurrent threads was required.
There are times when you might want to coordinate access to shared resources. For example, in a database system, you might not want one thread to be updating a database record while another thread is trying to read it. Java enables you to coordinate the actions of multiple threads using synchronized methods and synchronized statements.
An object for which access is to be coordinated is accessed through the use of synchronized methods. (Synchronized statements are covered in Chapter 11, "Language Summary.") These methods are declared with the synchronized keyword. Only one synchronized method can be invoked for an object at a given point in time. This keeps synchronized methods in multiple threads from conflicting with each other.
All classes and objects are associated with a unique monitor. The monitor is used to control the way in which synchronized methods are allowed to access the class or object. When a synchronized method is invoked for a given object, it is said to acquire the monitor for that object. No other synchronized method may be invoked for that object until the monitor is released. A monitor is automatically released when the method completes its execution and returns. A monitor may also be released when a synchronized method executes certain methods, such as wait(). The thread associated with the currently executing synchronized method becomes not runnable until the wait condition is satisfied and no other method has acquired the object's monitor.
The following example shows how synchronized methods and object monitors are used to coordinate access to a common object by multiple threads. This example adapts the ThreadTest1 program for use with synchronized methods, as shown in Listing 8.3.
Listing 8.3. The source code of the ThreadSynchronization program.import java.lang.Thread;
import java.lang.System;
import java.lang.Math;
import java.lang.InterruptedException;
class ThreadSynchronization {
public static void main(String args[]) {
MyThread thread1 = new MyThread("thread1: ");
MyThread thread2 = new MyThread("thread2: ");
thread1.start();
thread2.start();
boolean thread1IsAlive = true;
boolean thread2IsAlive = true;
do {
if(thread1IsAlive && !thread1.isAlive()){
thread1IsAlive = false;
System.out.println("Thread 1 is dead.");
}
if(thread2IsAlive && !thread2.isAlive()){
thread2IsAlive = false;
System.out.println("Thread 2 is dead.");
}
}while(thread1IsAlive || thread2IsAlive);
}
}
class MyThread extends Thread {
static String message[] = {"Java","is","hot,","aromatic,",
"and","invigorating."};
public MyThread(String id) {
super(id);
}
public void run() {
SynchronizedOutput.displayList(getName(),message);
}
void randomWait(){
try {
sleep((long)(3000*Math.random()));
}catch (InterruptedException x){
System.out.println("Interrupted!");
}
}
}
class SynchronizedOutput {
public static synchronized void displayList(String name,String list[]) {
for(int i=0;i<list.length;++i) {
MyThread t = (MyThread) Thread.currentThread();
t.randomWait();
System.out.println(name+list[i]);
}
}
}
Compile and run the program before going on with its analysis. You might be surprised at the results that you've obtained. Here are the results of an example run on my system:
C:\java\jdg\ch08>java ThreadSynchronization
thread1: Java
thread1: is
thread1: hot,
thread1: aromatic,
thread1: and
thread1: invigorating.
Thread 1 is dead.
thread2: Java
thread2: is
thread2: hot,
thread2: aromatic,
thread2: and
thread2: invigorating.
Thread 2 is dead.
Now edit ThreadSynchronization.java and delete the synchronized keyword in the declaration of the displayList() method of class SynchronizedOutput. It should look like this when you are finished:
class SynchronizedOutput {
public static void displayList(String name,String list[]) {
Save ThreadSynchronization.java, recompile it, and rerun it with the new change in place. You may now get output similar to this:
C:\java\jdg\ch08>java ThreadSynchronization
thread2: Java
thread1: Java
thread1: is
thread2: is
thread2: hot,
thread2: aromatic,
thread1: hot,
thread2: and
thread2: invigorating.
Thread 2 is dead.
thread1: aromatic,
thread1: and
thread1: invigorating.
Thread 1 is dead.
The difference in the program's output should give you a feel for the effects of synchronization upon multithreaded program execution. Let's analyze the program and explain these results.
The ThreadSynchronization class is essentially the same as the ThreadTest1 class. The only difference is the class name.
The MyThread class was modified slightly to allow for the use of the SynchronizedOutput class. Instead of the output being displayed in the run() method, as in ThreadTest1, the run() method simply invokes the displayList() method of the SynchronizedOutput class. It is important to understand that the displayList() method is static and applies to the SynchronizedOutput class as a whole, not to any particular instance of the class. The method displays the Java is hot, aromatic, and invigorating. message in the same manner as it was displayed in the previous examples of this chapter. It invokes randomWait() to wait a random amount of time before displaying each word in the message. The displayList() method uses the currentThread() method of class Thread to reference the current thread in order to invoke randomWait().
What difference, then, does the fact that displayList() is synchronized have on the program's execution? When displayList() is not synchronized, it may be invoked by one thread, say thread1, display some output, and wait while thread2 executes. When thread2 executes, it too invokes displayList() to display some output. Two separate invocations of displayList(), one for thread1 and the other for thread2, execute concurrently. This explains the mixed output display.
When the synchronized keyword is used, thread1 invokes displayList(), acquires a monitor for the SynchronizedOutput class (because displayList() is a static method), and displayList() proceeds with the output display for thread1. Because thread1 acquired a monitor for the SynchronizedOutput class, thread2 must wait until the monitor is released before it is able to invoke displayList() to display its output. This explains why one task's output is completed before the other's.
Java borrows the notion of a daemon thread from the UNIX daemon process. A daemon thread is a thread that executes in the background and provides services to other threads. It typically executes a continuous loop of instructions that wait for a service request, perform the service, and wait for the next service request. Daemon threads continue to execute until there are no more threads for which services can be provided. At this time, the daemon threads die and the Java interpreter terminates its execution. Any thread can be changed to a daemon thread using the setDaemon() method.
Thread groups are objects that consist of a collection of threads. Every thread is a member of a unique thread group. Thread groups are used to invoke methods that apply to all threads in the group. For example, a thread group can be used to start or stop all threads in a group, to change their priorities, or to change them to daemon threads.
A thread is entered into a thread group when it is created. After the thread enters a thread group, it remains a member of the group throughout its existence. A thread can never become a member of another group.
Threads are entered into a group using Thread constructors that take a ThreadGroup parameter. These constructors are described in the Thread class API documentation. If a thread's group is not specified in its constructor, as is the usual case, the thread is entered into the same group as the thread that created it. The default thread group for a newly executing Java application is the main group. All of the threads created in this chapter's examples have been members of the default main thread group. The ThreadGroup class is covered in Chapter 12.
In this chapter you have learned how to develop multithreaded programs using Java threads. You have learned how to synchronize multiple threads in order to share common resources. You have also learned how to use thread priorities to control thread scheduling. You have now covered the main features of the Java language. A complete language summary is provided in Chapter 11. In Chapter 9, "Using the Debugger," you'll learn how to use the Java debugger to help debug the programs you develop.