Monitors and Monitor Locks
Another way to perform synchronization is to use Java's built-in monitors. Every object has a monitor. The monitor allows one thread at a time to execute inside a synchronized statement on the object. This is accomplished by acquiring a lock on the object when the program enters the synchronized statement. These statements are declared using the synchronized keyword with the form
synchronized ( object )
{
statements
} // end synchronized statement
where object is the object whose monitor lock will be acquired. If there are several synchronized statements trying to execute on an object at the same time, only one of them may be active on the object at onceall the other threads attempting to enter a synchronized statement on the same object are placed in the blocked state.
The blocked state is not included in Fig. 23.1, but it transitions to and from the runnable state. 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.
When a synchronized statement finishes executing, the monitor lock on the object is released and the highest-priority blocked thread attempting to enter a synchronized statement proceeds. Java also allows synchronized methods. A synchronized method is equivalent to a synchronized statement enclosing the entire body of a method.
If a thread obtains the monitor lock on an object and then 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. The thread releases the monitor lock on the object and waits in the waiting state while the other threads try to enter the object's synchronized statement(s). When a thread executing a synchronized statement completes or satisfies the condition on which another thread may be waiting, it can call Object method notify to allow a waiting thread to transition to the blocked state again. At this point, the thread that transitioned from the wait state to the blocked state can attempt to reacquire the monitor lock on the object. Even if the thread is able to reacquire the monitor lock, it still might not be able to perform its task at this timein which case the thread will reenter the waiting state and release the monitor lock. If a thread calls notifyAll, then all the threads waiting for the monitor lock become eligible to reacquire the lock (that is, they all transition to the blocked state). Remember that only one thread at a time can obtain the monitor lock on the objectother threads that attempt to acquire the same monitor lock will be blocked until the monitor lock becomes available again (i.e., until no other thread is executing in a synchronized statement on that object). Methods wait, notify and notifyAll are inherited by all classes from class Object.
Software Engineering Observation 23.3
The locking that occurs with the execution of synchronized methods could lead to deadlock if the locks are never released. When exceptions occur, Java's exception mechanism coordinates with Java's synchronization mechanism to release locks and avoid these kinds of deadlocks. |
Common Programming Error 23.5
It is an error if a thread issues a wait, a notify or a notifyAll on an object without having acquired a lock for it. This causes an IllegalMonitorStateException. |
The application in Fig. 23.19 and Fig. 23.20 demonstrates a producer and a consumer accessing a shared buffer with synchronization. In this case, the consumer consumes only after the producer produces a value, and the producer produces a new value only after the consumer consumes the value produced previously. In this example, we reuse interface Buffer (Fig. 23.6) and classes Producer (Fig. 23.7) and Consumer (Fig. 23.8) from the example in Section 23.6. The code that performs the synchronization is placed in the set and get methods of class SynchronizedBuffer (Fig. 23.19), which implements interface Buffer (line 4). Thus, the Producer's and Consumer's run methods simply call the shared object's set and get methods, as in the example in Section 23.6.
Figure 23.19. Synchronizes access to shared data using Object methods wait and notify.
(This item is displayed on pages 1095 - 1096 in the print version)
1 // Fig. 23.19: SynchronizedBuffer.java 2 // SynchronizedBuffer synchronizes access to a single shared integer. 3 4 public class SynchronizedBuffer implements Buffer 5 { 6 private int buffer = -1; // shared by producer and consumer threads 7 private boolean occupied = false; // count of occupied buffers 8 9 // place value into buffer 10 public synchronized void set( int value ) 11 { 12 // while there are no empty locations, place thread in waiting state 13 while ( occupied ) 14 { 15 // output thread information and buffer information, then wait 16 try 17 { 18 System.out.println( "Producer tries to write." ); 19 displayState( "Buffer full. Producer waits." ); 20 wait(); 21 } // end try 22 catch ( InterruptedException exception ) 23 { 24 exception.printStackTrace(); 25 } // end catch 26 } // end while 27 28 buffer = value; // set new buffer value 29 30 // indicate producer cannot store another value 31 // until consumer retrieves current buffer value 32 occupied = true; 33 34 displayState( "Producer writes " + buffer ); 35 36 notify(); // tell waiting thread to enter runnable state 37 } // end method set; releases lock on SynchronizedBuffer 38 39 // return value from buffer 40 public synchronized int get() 41 { 42 // while no data to read, place thread in waiting state 43 while ( !occupied ) 44 { 45 // output thread information and buffer information, then wait 46 try 47 { 48 System.out.println( "Consumer tries to read." ); 49 displayState( "Buffer empty. Consumer waits." ); 50 wait(); 51 } // end try 52 catch ( InterruptedException exception ) 53 { 54 exception.printStackTrace(); 55 } // end catch 56 } // end while 57 58 // indicate that producer can store another value 59 // because consumer just retrieved buffer value 60 occupied = false; 61 62 int readValue = buffer; // store value in buffer 63 displayState( "Consumer reads " + readValue ); 64 65 notify(); // tell waiting thread to enter runnable state 66 67 return readValue; 68 } // end method get; releases lock on SynchronizedBuffer 69 70 // display current operation and buffer state 71 public void displayState( String operation ) 72 { 73 System.out.printf( "%-40s%d %b ", operation, buffer, 74 occupied ); 75 } // end method displayState 76 } // end class SynchronizedBuffer |
Figure 23.20. SharedBufferTest2 sets up a producer/consumer application that uses a synchronized buffer.
(This item is displayed on pages 1098 - 1100 in the print version)
1 // Fig 23.20: SharedBufferTest2.java 2 // Application shows two threads manipulating a synchronized buffer. 3 import java.util.concurrent.ExecutorService; 4 import java.util.concurrent.Executors; 5 6 public class SharedBufferTest2 7 { 8 public static void main( String[] args ) 9 { 10 // create new thread pool with two threads 11 ExecutorService application = Executors.newFixedThreadPool( 2 ); 12 13 // create SynchronizedBuffer to store ints 14 Buffer sharedLocation = new SynchronizedBuffer(); 15 16 System.out.printf( "%-40s%s %s %-40s%s ", "Operation", 17 "Buffer", "Occupied", "---------", "------ --------" ); 18 19 try // try to start producer and consumer 20 { 21 application.execute( new Producer( sharedLocation ) ); 22 application.execute( new Consumer( sharedLocation ) ); 23 } // end try 24 catch ( Exception exception ) 25 { 26 exception.printStackTrace(); 27 } // end catch 28 29 application.shutdown(); 30 } // end main 31 } // end class SharedBufferTest2
|
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.
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.
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.