Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

AhmadLang / Java, How To Program, 2004

.pdf
Скачиваний:
626
Добавлен:
31.05.2015
Размер:
51.82 Mб
Скачать

[Page 1055 (continued)]

23.2. Thread States: Life Cycle of a Thread

At any time, a thread is said to be in one of several thread states that are illustrated in the UML state diagram in Fig. 23.1. A few of the terms in the diagram are defined in later sections.

Figure 23.1. Thread life-cycle UML state diagram.

[View full size image]

A new thread begins its life cycle in the new state. It remains in this state until the program starts the thread, which places the thread in the runnable state. A thread in this state is considered to be executing its task.

Sometimes a thread transitions to the waiting state while the thread waits for another thread to perform a task. Once in this state, a thread transitions back to the runnable state only when another thread signals the waiting thread to continue executing.

A runnable thread can enter the timed waiting state for a specified interval of time. A thread in this state transitions back to the runnable state when that time interval expires or when the event it is waiting for occurs. Timed waiting threads cannot use a processor, even if one is available. A thread can transition to the timed waiting state if it provides an optional wait interval when it is waiting for another thread to perform a task. Such a thread will return to the runnable state when it is signaled by another thread or when the timed interval expireswhichever comes first. Another way to place a thread in the timed waiting state is to put the thread to sleep. A sleeping thread remains in the timed waiting state for a designated period of time (called a sleep interval) at which point it returns to the runnable state. Threads sleep when they momentarily do not have work to perform. For example, a word processor may contain a thread that periodically writes a copy of the current document to disk for recovery purposes. If the thread did not sleep between successive backups, it would require a loop in which it continually tests whether it should write a copy of the document to disk. This loop would consume processor time without performing productive work, thus reducing system performance. In this case, it is more efficient for the thread to specify a sleep interval (equal to the period between successive backups) and enter the timed waiting state. This thread is returned to the runnable state when its sleep interval expires, at which point it writes a copy of the document to disk and reenters the timed waiting state.

[Page 1056]

A runnable thread enters the terminated state when it completes its task or otherwise terminates (perhaps due to an error condition). In the UML state diagram in Fig. 23.1, the terminated state is followed by the UML final state (the bull's-eye symbol) to indicate the end of the state transitions.

At the operating-system level, Java's runnable state actually encompasses two separate states (Fig. 23.2). The operating system hides these two states from the Java Virtual Machine (JVM), which only sees the runnable state. When a thread first transitions to the runnable state from the new state, the thread is in the ready state. A ready thread enters the running state (i.e., begins executing) when the operating system assigns the thread to a processoralso known as dispatching the thread. In most operating systems, each thread is given a small amount of processor timecalled a quantum or timeslicewith which to perform its task. When the thread's quantum expires, the thread returns to the ready state and the operating system assigns another thread to the processor (see Section 23.3). Transitions between these states are handled solely by the operating system. The JVM does not "see" these two statesit simply views a thread as being in the runnable state and leaves it up to the operating system to transition threads between the ready and running states. The process that an operating system uses to decide which thread to dispatch is known as thread scheduling and is dependent on thread priorities (discussed in the next section).

Figure 23.2. Operating system's internal view of Java's runnable state.

[View full size image]

[Page 1056 (continued)]

23.3. Thread Priorities and Thread Scheduling

Every Java thread has a priority that helps the operating system determine the order in which threads are scheduled. Java priorities are in the range between MIN_PRIORITY (a constant of 1) and

MAX_PRIORITY (a constant of 10). Informally, threads with higher priority are more important to a program and should be allocated processor time before lower-priority threads. However, thread priorities cannot guarantee the order in which threads execute. By default, every thread is given priority

NORM_PRIORITY (a constant of 5). Each new thread inherits the priority of the thread that created it.

[Page 1057]

[Note: These constants (MAX_PRIORITY, MIN_PRIORITY and NORM_PRIORITY) are declared in the Thread class. It is recommended that you do not explicitly create and use Thread objects to implement concurrency, but rather use the Runnable interface (which is described in Section 23.4). The THRead class does contain some static methods that are useful, as you will see later in this chapter.]

Most Java platforms support timeslicing, which enables threads of equal priority to share a processor. Without timeslicing, each thread in a set of equal-priority threads runs to completion (unless it leaves the runnable state and enters the waiting or timed waiting state, or gets interrupted by a higher-priority thread) before other threads of equal priority get a chance to execute. With timeslicing, even if the thread has not finished executing when the quantum expires, the processor is taken away from that thread and given to the next thread of equal priority, if one is available.

The job of an operating system's thread scheduler is to determine which thread runs next. One simple implementation of the thread scheduler keeps the highest-priority thread running at all times and, if there is more than one highest-priority thread, ensures that all such threads execute for a quantum each in round-robin fashion. Figure 23.3 illustrates a multi-level priority queue for threads. In the figure, assuming a single-processor computer, threads A and B each execute for a quantum in roundrobin fashion until both threads complete execution. This means that A gets a quantum of time to run. Then B gets a quantum. Then A gets another quantum. Then B gets another quantum. This continues until one thread completes. The processor then devotes all its power to the thread that remains (unless another thread of that priority becomes ready). Next, thread C runs to completion (assuming that no higher-priority threads arrive). Threads D, E and F each execute for a quantum in round-robin fashion until they all complete execution (again assuming that no higher-priority threads arrive). This process continues until all threads run to completion.

Figure 23.3. Thread-priority scheduling.

(This item is displayed on page 1058 in the print version)

[View full size image]

Portability Tip 23.2

Thread scheduling is platform dependentan application that uses multithreading could behave differently on separate Java implementations.

When a higher-priority thread enters the ready state, the operating system generally preempts the currently running thread (an operation known as preemptive scheduling). Depending on the operating system, higher-priority threads could postponepossibly indefinitelythe execution of lower-priority threads. Such indefinite postponement often is referred to more colorfully as starvation.

Portability Tip 23.3

When designing applets and applications that use threads, you must consider the threading capabilities of all the platforms on which the applets and applications will execute.

J2SE 5.0 provides higher-level concurrency utilities to hide some complexity and make multithreaded programs less error prone (though they are still certainly complex). Thread priorities are still used behind the scenes to interact with the operating system, but most programmers who use J2SE 5.0 multithreading will not be concerned with setting and adjusting thread priorities. You can learn more about priorities and threading at java.sun.com/j2se/5.0/docs/api/java/lang/Thread.html.

// method run is the code to be executed by new thread
private int sleepTime; // random sleep time for thread private String threadName; // name of thread
private static Random generator = new Random();

[Page 1058]

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.

[Page 1059]

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.

Figure 23.4. Threads sleeping and awakening.

(This item is displayed on page 1060 in the print version)

1

//

Fig. 23.4: PrintTask.java

2

//

PrintTask class sleeps for a random time from 0 to 5 seconds

3

import java.util.Random;

4

 

 

5class PrintTask implements Runnable

6{

7

8

9

10

11// assign name to thread

12public PrintTask( String name )

13{

14threadName = name; // set name of thread

16// pick random sleep time between 0 and 5 seconds

17sleepTime = generator.nextInt( 5000 );

18} // end PrintTask constructor

20

21public void run()

22{

23try // put thread to sleep for sleepTime amount of time

24{

25System.out.printf( "%s going to sleep for %d milliseconds.\n",

26

threadName, sleepTime );

27

 

28Thread.sleep( sleepTime ); // put thread to sleep

29} // end try

30// if thread interrupted while sleeping, print stack trace

31catch ( InterruptedException exception )

32{

33exception.printStackTrace();

34 } // end catch

35

36// print thread name

37System.out.printf( "%s done sleeping\n", threadName );

38} // end method run

39} // end class PrintTask

When a PrintTask is assigned to a processor for the first time, its run method (lines 2138) begins execution. Lines 2526 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 sleepingthen method run terminates. Note that the catch at lines 3134 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 828) creates and names three PrintTask objects (lines 1113). 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.

Figure 23.5. Creates three PrintTasks and executes them.

(This item is displayed on page 1061 in the print version)

1 // Fig. 23.5: RunnableTester.java

2 // Multiple threads printing at different intervals.

3import java.util.concurrent.Executors;

4import java.util.concurrent.ExecutorService;

6public class RunnableTester

7{

8public static void main( String[] args )

9{

10// create and name each runnable

11PrintTask task1 = new PrintTask( "thread1" );

12PrintTask task2 = new PrintTask( "thread2" );

13PrintTask task3 = new PrintTask( "thread3" );

15System.out.println( "Starting threads" );

17// create ExecutorService to manage threads

18ExecutorService threadExecutor = Executors.newFixedThreadPool( 3 );

20// start threads and place in runnable state

21threadExecutor.execute( task1 ); // start task1

22threadExecutor.execute( task2 ); // start task2

23threadExecutor.execute( task3 ); // start task3

25threadExecutor.shutdown(); // shutdown worker threads

27System.out.println( "Threads started, main ends\n" );

28} // end main

29} // end class RunnableTester

Starting threads

Threads started, main ends

thread1 going to sleep for 1217 milliseconds thread2 going to sleep for 3989 milliseconds thread3 going to sleep for 662 milliseconds thread3 done sleeping

thread1 done sleeping thread2 done sleeping

Starting

threads

 

 

 

 

thread1

going

to

sleep

for

314

milliseconds

thread2

going

to

sleep

for

1990

milliseconds

Threads

started,

main

ends

 

 

thread3

going

to

sleep

for

3016

milliseconds

thread1

done

sleeping

 

 

 

thread2

done

sleeping

 

 

 

thread3

done

sleeping

 

 

 

 

 

 

 

 

 

 

Lines 2123 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 invocationthe 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.]

[Page 1060]

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 2138 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.

[Page 1061]

[Page 1062]

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.

[Page 1062 (continued)]

23.5. Thread Synchronization

Often, multiple threads of execution manipulate a shared object in memory. When multiple threads share an object and that object is modified by one or more of the threads, indeterminate results may occur (as we will soon see in the chapter examples) unless the shared object is managed properly. If one thread is in the process of updating a shared object and another thread tries to update it too, it is possible that part of the object will reflect the information from one thread while another part of the object reflects information from a different thread. When this happens, the program's behavior cannot be trusted. Sometimes the program will produce the correct results, but other times it will produce incorrect results. In either case there will be no error message to indicate that the shared object was manipulated incorrectly.

The problem can be solved by giving one thread at a time exclusive access to code that manipulates the shared object. During that time, other threads desiring to manipulate the object are kept waiting. When the thread with exclusive access to the object finishes manipulating it, one of the threads that was kept waiting is allowed to proceed. In this fashion, each thread accessing the shared object excludes all other threads from doing so simultaneously. This is called mutual exclusion. Mutual exclusion allows the programmer to perform thread synchronization, which coordinates access to shared data by multiple concurrent threads.

Java uses locks to perform synchronization. Any object can contain an object that implements the

Lock interface (package java.util.concurrent.locks). A thread calls the Lock's lock method to obtain the lock. Once a Lock has been obtained by one thread, the Lock object will not allow another thread to obtain the lock until the first thread releases the Lock (by calling the Lock's unlock method). If there are several threads trying to call method lock on the same Lock object at the same time, only one thread may obtain the lock at a timeall other threads attempting to obtain the Lock contained in the same object are placed in the waiting state for that lock. When a thread calls method unlock, the lock on the object is released and the highest-priority waiting thread attempting to lock the object proceeds. Class ReentrantLock (package java.util.concurrent.locks) is a basic implementation of the Lock interface. The constructor for a ReentrantLock takes a boolean argument that specifies whether the lock has a fairness policy. If this is set to true, the ReentrantLock's fairness policy states that the longest-waiting thread will acquire the lock when it is available. If this is set to false, there is no guarantee as to which waiting thread will acquire the lock when it is available.

[Page 1063]

Performance Tip 23.2

Using a Lock with a fairness policy helps avoid indefinite postponement, but can also dramatically reduce the overall efficiency of a program. Because of the large decrease in performance, fair locks are only necessary in extreme circumstances.

If a thread that owns the lock on an object determines that it cannot continue with its task until some condition is satisfied, the thread can wait on a condition variable. This removes the thread from contention for the processor, places it in a wait queue for the condition variable and releases the lock on the object. Condition variables must be associated with a Lock and are created by calling Lock method newCondition, which returns an object that implements the Condition interface (package java.util.concurrent.locks). To wait on a condition variable, the thread can call the Condition's

await method. This immediately releases the associated Lock and places the thread in the waiting state for that Condition. Other threads can then try to obtain the Lock. When a runnable thread completes a task and determines that the waiting thread can now continue, the runnable thread can call Condition method signal to allow a thread in that Condition's waiting state to return to the runnable state. At this point, the thread that transitioned from the waiting state to the runnable state can attempt to reacquire the Lock on the object. Even if it is able to reacquire the Lock, the thread still

might not be able to perform its task at this timein which case the thread can call method await to release the Lock and reenter the waiting state. If multiple threads are in a Condition's waiting state when signal is called, the default implementation of Condition signals the longest-waiting thread to move to the runnable state. If a thread calls Condition method signalAll, then all the threads waiting for that condition move to the runnable state and become eligible to reacquire the Lock. Only one of those threads can obtain the Lock on the object at a timeother threads that attempt to acquire the same Lock will wait until the Lock becomes available again. If the Lock was created with a fairness policy, the longest-waiting thread will then acquire the Lock. When a thread is finished with a shared object, it must call method unlock to release the Lock.

Common Programming Error 23.1

Deadlock occurs when a waiting thread (let us call this thread1) cannot proceed because it is waiting (either directly or indirectly) for another thread (let us call this thread2) to proceed, while simultaneously thread2 cannot proceed because it is waiting (either directly or indirectly) for thread1 to proceed. Two threads are waiting for each other, so the actions that would enable each thread to continue execution never occur.

Error-Prevention Tip 23.1

When multiple threads manipulate a shared object using locks, ensure that if one thread calls method await to enter the waiting state for a condition variable, a separate thread eventually will call Condition method signal to transition the thread waiting on the condition variable back to the runnable state. If multiple threads may be waiting on the condition variable, a separate thread can call Condition method signalAll as a safeguard to ensure that all the waiting threads have another opportunity to perform their tasks. If this is not done, indefinite postponement or deadlock could occur.

Software Engineering Observation 23.1

The locking that occurs with the execution of the lock and unlock methods could lead to deadlock if the locks are never released. Calls to method unlock should be placed in finally blocks to ensure that locks are released and avoid these kinds of deadlocks.

[Page 1064]

Performance Tip 23.3

Synchronization to achieve correctness in multithreaded programs can make programs run more slowly, as a result of thread overhead and the frequent transition of threads between the waiting and runnable states. There is not much to say, however, for highly efficient yet incorrect multithreaded programs!