AhmadLang / Java, How To Program, 2004
.pdf
Operation |
|
|
|
Buffer |
Occupied |
--------- |
|
|
|
------ |
-------- |
Consumer |
tries to |
read. |
|
|
|
Buffer empty. Consumer waits. |
-1 |
false |
|||
Producer |
writes |
1 |
|
1 |
true |
Consumer |
reads |
1 |
|
1 |
false |
Consumer |
tries to |
read. |
|
|
|
Buffer empty. Consumer waits. |
1 |
false |
|||
Producer |
writes |
2 |
|
2 |
true |
Consumer |
reads |
2 |
|
2 |
false |
Producer |
writes |
3 |
|
3 |
true |
Consumer |
reads |
3 |
|
3 |
false |
Consumer |
tries to |
read. |
|
|
|
Buffer empty. Consumer waits. |
3 |
false |
|||
Producer |
writes |
4 |
|
4 |
true |
Consumer |
reads |
4 |
|
4 |
false |
Consumer |
tries to |
read. |
|
|
|
Buffer empty. Consumer waits. |
4 |
false |
|||
Producer |
writes |
5 |
|
5 |
true |
Consumer |
reads |
5 |
|
5 |
false |
Producer |
writes |
6 |
|
6 |
true |
Consumer |
reads |
6 |
|
6 |
false |
Consumer |
tries to |
read. |
|
|
|
Buffer empty. Consumer waits. |
6 |
false |
|||
Producer |
writes |
7 |
|
7 |
true |
Consumer |
reads |
7 |
|
7 |
false |
Consumer |
tries to |
read. |
|
|
|
Buffer empty. Consumer waits. |
7 |
false |
|||
Producer |
writes |
8 |
|
8 |
true |
Consumer |
reads |
8 |
|
8 |
false |
Producer |
writes |
9 |
|
9 |
true |
Producer |
tries to |
write. |
|
|
|
Buffer full. Producer waits. |
9 |
true |
|||
Consumer |
reads |
9 |
|
9 |
false |
Producer |
writes |
10 |
|
10 |
true |
Producer |
done producing. |
|
|
||
Terminating Producer. |
|
|
|||
Consumer |
reads |
10 |
|
10 |
false |
Consumer read values totaling 55.
Terminating Consumer.
[Page 1096]
Class SynchronizedBuffer (Fig. 23.19) contains two fieldsbuffer (line 6) and occupied (line 7). Method set (lines 1037) and method get (lines 4068) are declared as synchronized methods by adding the synchronized keyword between the method modifier and the return typethus, only one thread can call any of these methods at a time on a particular SynchronizedBuffer object. Field occupied is used in conditional expressions to determine whether it is the producer's or the consumer's turn to perform a task. If occupied is false, buffer is empty and the producer can call method set to place a value into variable buffer. This condition also means that the consumer cannot call SynchronizedBuffer's get method to read the value of buffer because it is empty. If occupied is TRue, the consumer can call SynchronizedBuffer's get method to read a value from variable buffer, because the variable contains new information. This condition also means that the producer cannot call SynchronizedBuffer's set method to place a value into buffer, because the buffer is currently full.
When the Producer tHRead's run method invokes synchronized method set, the thread attempts to acquire the monitor lock on the SynchronizedBuffer object. If the monitor lock is available, the Producer thread acquires the lock. Then the while loop at lines 1326 determines whether occupied is true. If so, the buffer is full, so line 18 outputs a message indicating that the Producer thread is trying to write a value, and line 19 invokes method displayState (lines 7175) to output another message indicating that the buffer is full and that the Producer thread is in the waiting state. Line 20 invokes method wait (inherited from Object by SynchronizedBuffer) to place the thread that called method set (i.e., the Producer thread) in the waiting state for the SynchronizedBuffer object. The call to wait causes the calling thread to release the lock on the SynchronizedBuffer object. This is important because the thread cannot currently perform its task and because other threads should be allowed to access the object at this time to allow the condition (occupied) to change. Now another thread can attempt to acquire the SynchronizedBuffer object's lock and invoke the object's set or get method.
[Page 1097]
The producer thread remains in the waiting state until the thread is notified by another thread that it may proceedat which point the producer thread returns to the blocked state and attempts to reacquire the lock on the SynchronizedBuffer object. If the lock is available, the producer thread reacquires the lock, and method set continues executing with the next statement after wait. Because wait is called in a loop (lines 1326), the loop-continuation condition is tested again to determine whether the thread can proceed with its execution. If not, wait is invoked againotherwise, method set continues with the next statement after the loop.
Line 28 in method set assigns value to buffer. Line 32 sets occupied to true to indicate that the buffer now contains a value (i.e., a consumer can read the value, and a producer cannot yet put another value there). Line 34 invokes method displayState to output a line to the console window indicating that the producer is writing a new value into the buffer. Line 36 invokes method notify (inherited from Object). If there are any waiting threads, the first one enters the blocked state, indicating that the thread can now attempt to acquire the lock again. Method notify returns immediately and method set returns to its caller. Invoking method notify works correctly in this program because only one thread calls method get at any time (the ConsumerThread). In programs that have multiple threads waiting on a condition, it may be more appropriate to use method notifyAll or call method wait with an optional timeout. When method set returns, it implicitly releases the lock on the shared memory.
Methods get and set are implemented similarly. When the Consumer thread's run method invokes synchronized method get, the thread attempts to acquire the monitor lock on the SynchronizedBuffer object. If the lock is available, the Consumer thread acquires it. Then the while loop at lines 4356 determines whether occupied is false. If so, the buffer is empty, so line 48 outputs a message indicating that the Consumer thread is trying to read a value, and line 49 invokes method displayState to output another message indicating that the buffer is empty and that the Consumer tHRead is waiting. Line 50 invokes method wait to place the thread that called method get (i.e., the Consumer thread) in the waiting state for the SynchronizedBuffer object. Again, the call to wait causes the calling thread to release the lock on the SynchronizedBuffer object, so another thread can attempt to acquire the SynchronizedBuffer object's lock and invoke the object's set or get method. If the lock on the SynchronizedBuffer is not available (e.g., if the ProducerThread has not yet returned from method set), the ConsumerThread is blocked until the lock becomes available.
The consumer thread object remains in the waiting state until the thread is notified by another thread that it may proceedat which point the consumer thread returns to the blocked state and attempts to reacquire the lock on the SynchronizedBuffer object. If the lock is available, the consumer thread reacquires the lock and method get continues executing with the next statement after wait. Because wait is called in a loop (lines 4356), the loop-continuation condition is tested again to determine whether the thread can proceed with its execution. If not, wait is invoked againotherwise, method get continues with the next statement after the loop. Line 60 sets occupied to false to indicate that buffer is now empty (i.e., a consumer cannot read the value, but a producer can place another value into buffer), line 63 calls method
displayState to indicate that the consumer is reading and line 65 invokes method notify. If there are any threads in the blocked state for the lock on this SynchronizedBuffer object, one of them enters the runnable state, indicating that the thread can now attempt to reacquire the lock and continue performing its task. Method notify returns immediately, then method get returns the value of buffer to its caller. Invoking method notify works correctly in this program because only one thread calls method set at any time (the ProducerThread). Programs that have multiple threads waiting on a condition should invoke notifyAll to ensure that multiple threads receive notifications properly. When method get returns, the lock on the SynchronizedBuffer object is implicitly released.
[Page 1098]
Class SharedBufferTest2 (Fig. 23.20) is identical to class SharedBufferTest (Fig. 23.12). Study the outputs in Fig. 23.20. Observe that every integer produced is consumed exactly onceno values are lost, and no values are consumed more than once. The synchronization and condition variable ensure that the producer and consumer cannot perform their tasks unless it is their turn. The producer must go first, the consumer must wait if the producer has not produced since the consumer last consumed, and the producer must wait if the consumer has not yet consumed the value that the producer most recently produced. Execute this program several times to confirm that every integer produced is consumed exactly once. In the sample output, note the lines indicating when the producer and consumer must wait to perform their respective tasks.
[Page 1100]
[Page 1100 (continued)]
23.13. Wrap-Up
This chapter presented Java's new concurrency API and demonstrated the powerful technique of thread synchronization. Multithreading is an advanced topic that is the subject of many books. We use the multithreading techniques introduced here again in Chapter 24, Networking, to help build multithreaded servers that can interact with multiple clients concurrently.
[Page 1100 (continued)]
Summary
Computers perform operations concurrently, such as compiling programs, sending files to a printer and receiving electronic mail messages over a network.
Programming languages generally provide a set of control statements that only enable programmers to perform one action at a time.
Historically, concurrency has been implemented as operating-system primitives available only to experienced systems programmers.
Java makes concurrency available to the applications programmer. The programmer specifies that
applications contain threads of executioneach thread designating a portion of a program that may execute concurrently with other threads. This capability is called multithreading.
A new thread begins its life cycle in the new state. It remains in the new state until the program starts the thread, which places the thread in the runnable state.
A runnable thread enters the terminated state when it completes its task or otherwise terminates.
[Page 1101]
Sometimes a thread transitions to the waiting state while it 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. 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.
At the operating system level, the runnable state actually encompasses two separate states.
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 processor. When the thread's quantum expires, the thread returns to the ready state, and the operating system assigns to the processor another thread.
Every Java thread has a priority in the range between MIN_PRIORITY (1) and MAX_PRIORITY (10). By default, each thread is given priority NORM_PRIORITY (5).
Most Java platforms support timeslicing. Without timeslicing, each thread in a set of equal-
priority threads runs to completion before other equal priority threads get a chance to execute. With timeslicing, each thread receives a brief burst of processor time, or quantum, during which the thread can execute. When the quantum expires, even if the thread has not finished executing, the processor is taken away from it and given to the next thread of equal priority, if one is available.
The thread scheduler determines which thread runs next. One simple implementation will keep
the highest-priority thread running at all times. If there is more than one highest-priority thread, the scheduler ensures that all such threads execute for a quantum each in round-robin fashion.
Multithreading in Java is accomplished by implementing the Runnable interface, which declares a single method named run.
Runnables are executed using a class that implements the Executor interface, which declares a single method named execute.
Interface ExecutorService is a subinterface of Executor that declares several 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.
ExecutorService method shutdown ends each thread in an ExecutorService as soon as it finishes executing its Runnable.
When multiple threads share an object, indeterminate results may occur unless the shared object
is synchronization properly. Mutual exclusion allows the programmer to perform such thread synchronization.
Once a Lock has been obtained by a thread (by calling the lock method), the Lock object will
not allow another thread to obtain the lock until the first thread releases the Lock (by calling the unlock method). 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.
Once a thread obtains the lock on an object, if the thread determines that it cannot continue
with its task until some condition is satisfied, the thread can wait on a condition variable, thus removing it from contention for the processor and releasing the lock on the object. Condition variables are created by calling Lock method newCondition, which returns a Condition object.
A thread can call method await on a Condition object to release the associated Lock and place
that thread in the waiting state while other threads try to obtain the Lock. When another thread satisfies the condition on which the first thread is waiting, that thread can call Condition method signal to allow the waiting thread to transition to the runnable state again. If a thread calls Condition method signalAll, then all the threads waiting for the lock 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 have to wait until the lock becomes available again.
[Page 1102]
In a producer/consumer relationship, the producer portion of an application generates data and
stores it in a shared object, and the consumer portion of an application reads data from the shared object. In a multithreaded producer/consumer relationship, a producer thread generates data and places it in a shared object called a buffer. A consumer thread reads data from the buffer.
To minimize the amount of waiting time for threads that share resources and operate at the
same average speeds, use a circular buffer that provides extra buffer space into which the producer can place values and from which the consumer can retrieve those values.
The BlockingQueue interface declares methods put and take, which are the blocking equivalents
of Queue methods offer and remove, respectively. This means that method put will place an element at the end of the BlockingQueue, waiting if the queue is full. Method take will remove an element from the head of the BlockingQueue, waiting if the queue is empty. Class ArrayBlockingQueue implements the BlockingQueue interface using an array. This makes the data structure fixed size, meaning that it will not expand to accommodate extra elements.
Swing components are not thread safeif multiple threads manipulate a Swing GUI component,
the results may not be correct. All interactions with Swing GUI components should be performed in the event-dispatch thread. Class SwingUtilities provides static method invokeLater to help with this process. Method invokeLater receives as its argument an object that implements interface Runnable and performs the GUI updates.
The Callable interface declares a single method named call which allows the user to return a
value or throw a checked exception. The ExecutorService interface provides method submit which will execute a Callable passed in as its argument. The submit method returns an object of type Future which is an interface that represents the executing Callable. The Future interface declares method get to return the result of the Callable and provides other methods to manage the execution of the Callable.
An object's monitor allows one thread at a time to execute inside a synchronized statement on that object.
When a runnable thread must wait to enter a synchronized statement, it transitions to the
blocked state. When the blocked thread enters the synchronized statement, it transitions to the runnable state.
Java also allows synchronized methods which is equivalent to a synchronized statement that encloses the entire body of the method.
Once a thread obtains the monitor lock on an object, if the thread determines that it cannot
continue with its task on that object until some condition is satisfied, the thread can call Object method wait, releasing the monitor lock on the object.
When a thread executing a synchronized statement completes or satisfies the condition on
which another thread may be waiting, the thread can call Object method notify to allow a waiting thread to transition to the blocked state again.
If a thread calls notifyAll, then all threads waiting for the monitor lock become eligible to reacquire the lock (that is, they all transition to the blocked state).
[Page 1102 (continued)]
Terminology
ArrayBlockingQueue class
await method of interface Condition blocked state
BlockingQueue interface
[Page 1103]
buffer
call method of interface Callable Callable interface
circular buffer concurrency
concurrent programming
Condition interface condition variable consumer consumer thread deadlock
dispatching a thread event-dispatching thread
execute method of interface Executor Executor interface
Executors class
ExecutorService interface
fairness policy of a lock
Future interface
garbage collection
garbage-collector thread
get method of interface Future
IllegalMonitorStateException class
indefinite postponement interrupt method of class THRead InterruptedException class
invokeLater method of class SwingUtilities Lock interface
lock method of interface Lock main thread
memory leak monitor multithreading mutual exclusion new state
newCachedThreadPool method of class Executors newCondition method of interface Lock newFixedThreadPool method of class Executors obtain the lock
notify method of class Object notifyAll method of class Object parallel operations
preemptive scheduling priority of a thread producer
producer thread producer/consumer relationship
put method of interface BlockingQueue
quantum
ready state
ReentrantLock class resume a thread round-robin scheduling
run method of interface Runnable Runnable interface
runnable state running state
shutdown method of class ExecutorService signal method of class Condition signalAll method of class Condition sleep interval
sleep method of class Thread sleeping thread
starvation
submit method of class ExecutorService suspend a thread
SwingUtilities class
synchronization synchronized keyword synchronized method synchronized statement
take method of interface BlockingQueue terminated state
thread
THRead class thread pool thread scheduler thread scheduling thread state
