This four-part tutorial introduces multithreading in Java 5.0. You'll learn about the new thread-state model and the priority-based thread scheduler. We'll demonstrate how to create and execute threads by using the new classes Executors and ExecutorService from the java.util.concurrent package. You'll also learn how to create thread pools, which enable threads to be reused after they complete their tasks. This tutorial is intended for students who are already familiar with Java and for Java developers.
[Note: This series of four tutorials (Part 1, Part 2, Part 3, Part 4) is an excerpt (Sections
23.1-23.4) of Chapter 23, Multithreading, from our textbook Java How to Program, 6/e. These
tutorials may refer to other chapters or sections of the book that are not included here. Permission Information: Deitel, Harvey M. and Paul J., JAVA HOW TO PROGRAM, ©2005, pp.1053-1058. Electronically reproduced by permission of Pearson Education, Inc., Upper Saddle River, New Jersey.]
23.4 Creating and Executing Threads
In J2SE 5.0, the preferred means of creating a multithreaded application is to implement the Runnable interface (package java.lang) and use built-in methods and classes to create Threads that execute the Runnables. The Runnable interface declares a single method named run. Runnables are executed by an object of a class that implements the Executor interface (package java.util.concurrent). This interface declares a single method named execute. An Executor object typically creates and manages a group of threads called a thread pool. These threads execute the Runnable objects passed to the execute method. The Executor assigns each Runnable to one of the available threads in the thread pool. If there are no available threads in the thread pool, the Executor creates a new thread or waits for a thread to become available and assigns that thread the Runnable that was passed to method execute. Depending on the Executor type, there may be a limit to the number of threads that can be created. Interface ExecutorService (package java.util.concurrent) is a subinterface of Executor that declares a number of other methods for managing the life cycle of the Executor. An object that implements the ExecutorService interface can be created using static methods declared in class Executors (package java.util.concurrent). We use these interfaces and methods in the next application, which executes three threads.
Class PrintTask (Fig. 23.4) implements Runnable (line 5), so that each PrintTask object can execute concurrently. Variable sleepTime (line 7) stores a random integer value (line 17) chosen when the PrintTask constructor executes. Each thread running a PrintTask object sleeps for the amount of time specified by the corresponding PrintTask object’s sleepTime, then outputs its name.
|
||
Fig. 23.4 | Threads sleeping and awakening. |
When a PrintTask is assigned to a processor for the first time, its run method (lines 21–38) begins execution. Lines 25–26 display a message indicating the name of the currently executing thread and stating that the thread is going to sleep for a certain number of milliseconds. Line 26 uses the threadName field which was initialized at line 14 with the PrintTask constructor’s argument. Line 28 invokes static method sleep of class Thread to place the thread into the timed waiting state. At this point, the thread loses the processor, and the system allows another thread to execute. When the thread awakens, it reenters the runnable state. When the PrintTask is assigned to a processor again, line 37 outputs the thread’s name in a message that indicates the thread is done sleeping—then method run terminates. Note that the catch at lines 31–34 is required because method sleep might throw an InterruptedException, which is a checked exception. Such an exception occurs if a sleeping thread’s interrupt method is called. Most programmers do not directly manipulate Thread objects, so InterruptedExceptions are unlikely to occur.
Figure 23.5 creates three threads of execution using the PrintTask class. Method main (lines 8–28) creates and names three PrintTask objects (lines 11–13). Line 18 creates a new ExecutorService. This line uses the newFixedThreadPool method of class Executors, which creates a pool consisting of a fixed number of Threads as indicated by the method’s argument (in this case, 3). These Threads are used by threadExecutor to execute the Runnables. If method execute is called and all the threads in the ExecutorService are being used, the Runnable will be placed in a queue and assigned to the first thread that completes its previous task. Executors method newCachedThreadPool returns an ExecutorService that creates new threads as they are needed by the application.
|
||
Fig. 23.5 | Creates three PrintTasks and executes them. |
|
|
Lines 21–23 invoke the ExecutorService’s execute method. This method creates a new Thread inside the ExecutorService to run the Runnable passed to it as an argument (in this case a PrintTask) and transitions that Thread from the new state to the runnable state. Method execute returns immediately from each invocation—the program does not wait for each PrintTask to finish. Line 25 calls ExecutorService method shutdown, which will end each Thread in threadExecutor as soon as each finishes executing its Runnable. Line 27 outputs a message indicating that the threads were started. [Note: Line 18 creates the ExecutorService using method newFixedThreadPool and the argument 3. This program executes only three Runnables, so a new Thread will be created by the ExecutorService for each Runnable. If the program executed more than three Runnables, additional Threads would not be created, but rather an existing Thread would be reused when it completed the Runnable assigned to it.]
The code in method main executes in the main thread. This thread is created by the JVM and executes the main method. The code in the run method of PrintTask (lines 21–38 of Fig. 23.4) executes in the threads created by the ExecutorService. When method main terminates (line 28), the program itself continues running because there are still threads that are alive (i.e., the threads started by threadExecutor that have not yet reached the terminated state). The program will not terminate until its last thread completes execution.
The sample outputs for this program show each thread’s name and sleep time as the thread goes to sleep. The thread with the shortest sleep time normally awakens first, indicates that it is done sleeping and terminates. In Section 23.8, we discuss multithreading issues that could prevent the thread with the shortest sleep time from awakening first. In the first output, the main thread terminates before any of the other threads output their names and sleep times. This shows that the main thread runs to completion before any of the other threads get a chance to run. In the second output, the first two threads output their names and sleep times before the main thread terminates. This shows that the operating system allowed other threads to execute before the main thread terminated. This is an example of the round-robin scheduling we discussed in Section 23.3.