Thread Synchronization

   

Up to this point in the discussion, the threads haven't needed to interact with other threads. However, there are many cases in which threads need to work together to perform a larger task. For example, if you have two threads, one is creating or "producing" data and the other thread needs to read or " consume " that data. This problem is commonly known as the producer/consumer problem. One thread is the producer of the data and the second thread is considered the consumer. As you learned previously, there is no guarantee which thread will be running at any given instance of time. In fact, the producer thread can be right in the middle of creating some data and be interrupted by the consumer thread. What would the data look like? It's really undeterminable. The problem here is really one of accessing shared data.

The other problem to consider occurs when one thread is faster than the other thread or is getting more processor attention. What happens if the producer thread is able to create data faster than the consumer thread can consumer it? Some of the data might be missed if it can't be persisted somehow until the consumer is ready to read more. In this case, the threads must communication better. The consumer thread should tell the producer thread that it's ready to read more. You'll see a solution for this problem later in this section. For now, take a look at how you can solve the problem of ensuring that a single thread will not be interrupted by another thread until it has completed a certain task.

The sections of an application that are accessed by separate threads are known as " critical sections. " In these areas, programmers must be cautious to protect multiple threads from interrupting each other too early. That protection can be achieved in Java by using the synchronized keyword. There are two ways that the keyword synchronized can be placed in Java, one is on a method and the second is by synchronizing a section or block of code. Placing synchronized on a method looks like this:

 public synchronized void writeSharedData( Object data )   {     // Work to write the shared data   } 

Notice the synchronized keyword added to the method signature. When a class defines a method as being synchronized, the Java VM associates a lock with each instance of the class that is instantiated . When a thread calls a synchronized method on a particular instance, no other thread can access that method or any other synchronized method defined in that class. The thread that calls and enters the critical section gets the lock on the object. This ensures that the first thread continues to execute until it completes with that method before allowing other threads to access it. Other threads are still free to access any non-synchronized method on the instance, just not the ones declared as synchronized. The tricky part when developing the application is identifying which parts of the code are the critical sections. For example, if there were a corresponding get method for the shared data object, you would want to protect that with a synchronized method like this:

 public synchronized Object readSharedData()   {     // Work to read and return the shared data   }   public synchronized void writeSharedData( Object data )   {     // Work to write the shared data   } 

Caution

Make sure you protect all critical sections in your application to ensure that multiple threads are not accessing shared data simultaneously .


The other way the synchronized keyword can be used in Java is by synchronizing on a block of code. Here is an example of this:

 public String getValue() {    // No lock acquired yet so other threads can access this instance    // while the other thread is doing this work    synchronized( this )    {      // A lock is obtained here by the calling thread and will remain until the      // thread leaves this block of code    }    return value; } 

The parameter to the synchronized block is the object on which the lock will be obtained. You can, of course, use any instance here, but in most cases the object that will need to be locked will be the current instance object.

The reason for this alternative method of synchronization revolves around the concept of deadlocks. Remember that when you synchronize a method, no other threads can enter that or any other synchronized method on that same instance until the thread with the lock is finished. If the thread with the lock needed to access another object that was locked by a different thread, each thread would be waiting on each other and neither one would be able to do anything. This is known as a deadlock condition, which you want to avoid at all costs. Preventing your application from entering a deadlock condition will be easier than detecting when you are in this condition and recovering from it gracefully.

Let's take a look at a complete example of synchronizing threads. The following example uses a shared object that each thread will need to access. The producer thread will need to write to the shared object, and the consumer thread will need to read from it. For now, just ignore what happens if either one of the threads works faster or slower than the other. You'll learn how to deal with that later in this section when the discussion turns to communication among threads using the notify and wait methods . For now, concentrate on synchronizing access to a share object or data.

Listing 11.10 shows the code you will work with for a while. The SharedObject class is the shared object data that both the producer and consumer will communicate with. Notice that the get and set methods are synchronized to protect two threads from accessing the shared data at the same time.

Listing 11.10 Source Code for SharedObject.java
 public class SharedObject {   // The private shared data variable   private Integer sharedData = null;   // Default Constructor   public SharedObject( int initialValue )   {     super();     // Initialize the shared object value to something     sharedData = new Integer( initialValue );   }   // The producer will call this method to update the data   public synchronized void setSharedData( int newData ) {     System.out.println( "Shared Object - New Value Set: " + newData );     sharedData = new Integer( newData );   }   // The consumer will call this method to get the new data   public synchronized Integer getSharedData()   {     return sharedData;   } } 

Listing 11.11 shows the class for the producer thread. It is initialized with the reference to the shared data object and a maximum number of writes that it should do. For this example, a loop is used to go from 1 up to the maximum and an Integer object wrapper is created around the loop counter. Notice that you don't have to do anything special when dealing with synchronization. All that was handled in the shared data class.

Listing 11.11 Source Code for SharedObjectProducer.java
 public class SharedObjectProducer extends Thread {   int maxCounter = 0;   SharedObject sharedObject = null;   public SharedObjectProducer( SharedObject obj, int maxWrites )   {     super();     sharedObject = obj;     maxCounter = maxWrites;   }   public void run()   {     for( int counterValue = 1; counterValue <= maxCounter; counterValue++ )     {       System.out.println( "Producer - Writing New Value: " + counterValue );       sharedObject.setSharedData( counterValue );     }   } } 

Listing 11.12 shows the counterpart to the producer, the consumer of the shared data. It is initialized with a reference to the same shared data object as the producer, and with a maximum number of times to read from it. These classes could have been designed to figure out when to stop writing and reading in many different ways. The point here is to see an example of synchronizing shared data between threads. Notice again that you don't have to do anything special to detect for other threads accessing the shared data. All of it was handled by using the synchronized keyword in the SharedObject class.

Listing 11.12 Source Code for SharedObjectConsumer.java
 public class SharedObjectConsumer extends Thread {   // The reference to the shared data   SharedObject sharedObject = null;   // How many reads of the shared data should happen   int numberOfReads = 0;   public SharedObjectConsumer( SharedObject obj, int numberOfTimesToRead )   {     super();     sharedObject = obj;     numberOfReads = numberOfTimesToRead;   }   private int getNumberOfReads()   {     return numberOfReads;   }   public void run()   {     int maxCounter = getNumberOfReads();     for( int counter = 1; counter <= maxCounter; counter++ )     {       Integer intObj = sharedObject.getSharedData();       System.out.println( "Consumer - Getting New Value: " + intObj.intValue() );     }   } } 

Listing 11.13 shows the class that gets it all started in testing these classes. Its job is to help you test the producer and consumer classes. The class creates a single instance of the SharedObject class and a single instance of both the producer and consumer classes. It then starts the produced and consumer threads.

Listing 11.13 Source Code for SharedObjectMain.java
 public class SharedObjectMain {   public static void main(String[] args)   {     int MAX_COUNTER = 5;     SharedObject sharedObject = new SharedObject( 0 );     SharedObjectProducer producer =  new SharedObjectProducer( sharedObject, MAX_COUNTER );     SharedObjectConsumer consumer = new SharedObjectConsumer( sharedObject, MAX_COUNTER );     producer.start();     consumer.start();   } } 

Running the SharedObjectMain class, here is what the output should look like:

 C:\jdk1.3se_book\classes>java SharedObjectMain Producer - Writing New Value: 1 Shared Object - New Value Set: 1 Producer - Writing New Value: 2 Shared Object - New Value Set: 2 Producer - Writing New Value: 3 Shared Object - New Value Set: 3 Producer - Writing New Value: 4 Shared Object - New Value Set: 4 Producer - Writing New Value: 5 Shared Object - New Value Set: 5 Consumer - Getting New Value: 5 Consumer - Getting New Value: 5 Consumer - Getting New Value: 5 Consumer - Getting New Value: 5 Consumer - Getting New Value: 5 Consumer - Getting New Value: 5 C:\jdk1.3se_book\classes> 

If the output does not match exactly with yours, don't worry. Again, this is due to the randomness of threads, operating systems, and other features of where you are running this example. This example demonstrates when multiple threads needs to access the same object, the critical sections needs to be synchronized. There is a really interesting thing in this output however. Notice the first data value that the consumer reads and prints out. Looking at the output in the previous code sample, you see that the first value is 5!

Hopefully you are asking what happened to 1, 2, 3, and 4. What happened is that because the producer thread got started first and maybe got a little more attention than the consumer thread, the producer thread just went right along writing values without ever checking to see that the consumer got them. This can be a big problem in applications if all the data needed to be sent to the consumer. You solve this problem with thread communication, which is discussed next .

Communicating Between Threads

As you saw in the last example, the consumer thread and the producer thread were not really aware of each other. And because they were not aware of each other and had no idea when one was writing and one was reading, they were not able to communicate. You need a way for the producer thread to notify the consumer thread that a new value has been written. You also need a way for the consumer thread to wait on a new value to be written and then signal to the producer thread that the new value has been read. Fortunately, Java has is able to do this with the wait and notify methods that exist on the Object class.

The wait method on the Object class causes the current thread to wait until another thread invokes the notify or notifyAll method on the current object. There are three variations of the wait method, the second and third take parameters that specify how long to wait, whereas the wait method with no parameters will wait indefinitely. When another thread calls the notify or notifyAll methods, an InterruptedException will be thrown. Therefore, you must be sure to catch this exception to know when another thread has signaled.

The nofityAll method wakes up all threads that are waiting on the lock for this object. The thread(s) that are notified when this method is called are not allowed to access the object until the current thread relinquishes control by getting out of the synchronized method or block. At that point, if there are multiple threads wanting access to the critical section, they must compete for it in the usual way by calling the synchronized method and acquiring a lock.

Caution

Only a thread that has acquired a lock on an object is able to call these methods. If a thread, which does not have a lock on the object, attempts to call one of the variations of these methods, a IllegalMonitorStateException will be thrown. A thread acquires the lock through the synchronization techniques discussed earlier.


Extend the previous example by creating a new class called BetterSharedObject. You can extend the previous SharedObject class to make it easier on yourself and to gain a little reuse. Listing 11.14 shows the new class.

Listing 11.14 Source Code for BetterSharedObject.java
 public class BetterSharedObject extends SharedObject {   // The private shared data variable   private Integer sharedData = null;   private boolean dataAvailable = false;   // Default Constructor   public BetterSharedObject( int initialValue )   {     super( initialValue );     // Initialize the shared object value to something     sharedData = new Integer( initialValue );   }   // The producer will call this method to update the data public synchronized void setSharedData( int newData )   {     if ( dataAvailable )     {       try       {         wait();       }       catch( InterruptedException ex )       {}     }     System.out.println( "Shared Object - New Value Set: " + newData );     sharedData = new Integer( newData );     System.out.println( "Send notification that it is ok to read data" ); dataAvailable = true;     notifyAll();   }   // The consumer will call this method to get the new data   public synchronized Integer getSharedData()   {     if ( !dataAvailable )     {       try       {         wait();       }       catch( InterruptedException ex )       {         System.out.println( "Got notification that it was ok to read data" );       }     }     dataAvailable = false;     notifyAll();     return sharedData;   } } 

There are really three changes that you should note with the new class. One is the new instance variable called dataAvailable. This boolean variable is used to provide an additional message between the two threads about when data is actually present in the sharedObject. If you just depended on the wait and notifyAll methods in this class because both the get and set methods need to wait in the beginning of the methods, neither would do anything. They would just sit and wait on each other. This is a case of deadlock. However, when you add the dataAvailable flag and initialize it to false, the set method, which is called by the producer thread, sees that there is no data available and jumps down past the wait method and sets the data. It then calls the notifyAll method to signal all the threads that the lock is being released and they can come after the sharedData.

You need to change the SharedObjectMain class to use the BetterSharedObject rather than the SharedObject class. When you run the SharedObjectMain, the output looks like this:

 C:\jdk1.3se_book\classes>java SharedObjectMain Producer - Writing New Value: 1 Shared Object - New Value Set: 1 Send notification that its ok to read new data Producer - Writing New Value: 2 Consumer - Getting New Value: 1 Shared Object - New Value Set: 2 Send notification that its ok to read new data Producer - Writing New Value: 3 Consumer - Getting New Value: 2 Shared Object - New Value Set: 3 Send notification that its ok to read new data Producer - Writing New Value: 4 Consumer - Getting New Value: 3 Shared Object - New Value Set: 4 Send notification that its ok to read new data Producer - Writing New Value: 5 Consumer - Getting New Value: 4 Shared Object - New Value Set: 5 Send notification that its ok to read new data Consumer - Getting New Value: 5 C:\jdk1.3se_book\classes> 

Notice that even though the producer is writing the new value, the shared object does not show that new value being sent until after the consumer has retrieved the previous value. This is because in the set method, it first checks to see if data from the previous set is still there. If it is, it calls the wait method until the consumer signals using the notifyAll method. This informs the producer thread that it's okay to write the next value. That's when you see the print statement from the SharedObject that the new value has been set.

Notice again that each of these two methods have to be synchronized as well so that each thread will have exclusive access to the object.

   


Special Edition Using Java 2 Standard Edition
Special Edition Using Java 2, Standard Edition (Special Edition Using...)
ISBN: 0789724685
EAN: 2147483647
Year: 1999
Pages: 353

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net