CSE 428: Lecture 16


Process synchronization in Java

Synchronized methods

Synchronized methods allow to encapsulate "critical sections", i.e. sequences of instructions that should not be executed "in parallel" with other instructions (executed by other threads) on the same object. "In parallel" here means "at the same time" and/or "in interleaving". Of course, if the program runs on a uniprocessor machine, then it is not possible to really execute two threads at the same time: parallelism can only be simulated by interleaving.

As an example of the necessity of dealing with critical sections, see the problem of the bank account in Arnold-Goslin, Ch 9.

Every object obj which has synchronized methods in its class declaration, is provided with a lock. When a thread T1 tries executing a synchronized method on obj, it must first acquire the lock. If in that moment the lock of obj is held by another thread T2 (executing the same method, or another synchronized method, on the same object obj), then T1 will block and wait for the lock. When T2 finally releases the lock on obj (which happens when the execution of the synchronized method activated by T2 is completed), the lock will be acquired by T1 or by some other thread waiting for the lock on obj, if any. The selection of the thread is made arbitrarily, i.e. it is transparent to the programmer.

Note that all synchronized methods on the same object are in mutual exclusion with each other, i.e. no synchronized method on the same object can be started by another thread before the previous execution of a synchonized method is completed. The same thread however is allowed to execute more than one synchronized method on the same object: there is a counter associated to a lock, which is incremented each time the thread enters a synchronized method on the object, and decremented when it completes a synchronized method. We will refer to the value of this counter as "level of nesting", for it is associated with the number of nested executions of synchronized methods on the same object (by the same thread).

Non synchronized methods, even on the same object, are not affected by this discipline: they can be executed completely independently from the lock, i.e. they can be executed in parallel with each other and also in parallel with synchronized methods.

Communication

It is often the case that a process needs to wait until a certain condition is verified. For instance, assume that we have a one-position buffer b, some processes "producer" that put (insert) data on b, and some processes "consumer" that get (read and remove) data from b. Before performing a put operation, each producer needs to wait until the buffer is empty. Analogously, before performing a get operation, each consumer needs to wait until the buffer contains a datum.

The problem could be solved in the following way:

This solution works, but it has a flaw: while attempting to do the put and the get operations, producer and consumer are in a state of "busy waiting", i.e. they keep being scheduled and consuming the processors time, just for repeating a test on a condition that might remain unsitisfied for a long time.

To solve this problem, Java provides the methods wait() and notifyAll(), to be used inside synchronized methods. Their meaning is the following:

The following program illustrates the use of wait() and notifyAll() to handle the situation of three producers and two consumers on the same buffer.

The method put(k) checks that the buffer is empty before updating its content to k. If it is not empty, then the calling thread (a producer) is put in the wait set. When the buffer is empty, the content is updated, the condition is_empty is set to false, and a notification is sent to all the processes waiting on the buffer. Of course, this notification is relevant only for the consumers.

The method get() checks that the buffer is not empty before returning its content. If it is empty, then the calling thread (a consumer) is put in the wait set. When the buffer is empty, the condition is_empty is set to true, a notification is sent to all the processes waiting on the buffer, and the content is returned. Of course, this notification is relevant only for the producers. Note that, even if the notification is not the last instruction executed in the get() method (the last one is "return content"), the resumption of the activity of a producer can happen only after the get() is completed, because both get() and put(k) are synchronized.

Note that all the calls to wait() are inserted in a while loop. This is essential for the correctness of the code. In fact, if we would replace the "while" with an "if", the condition on which the thread was suspended would not be tested again when the thread is resumed. This is incorrect since there might be several threads waiting on the same condition, and the first one which re-acquires the lock might change the condition again. So, when the other threads finally get the lock, there is no guarrantee that the condition is now satisfied.


/*              File Producers_Consumers.java                     */
/*   Three Producers and two consumers on a one-position buffer   */
 
/* ******** Main program ******** */
  
public class Producers_Consumers {
  public static void main(String[] args) {
    Buffer b = new Buffer();
    new Producer(b,1).start();
    new Producer(b,2).start();
    new Producer(b,3).start();
    new Consumer(b,1).start();
    new Consumer(b,2).start();
  }
}

/* ********** Producer ********* */

class Producer extends Thread {
  private Buffer buff;
  private int id;
  public Producer(Buffer b, int n) { buff = b; id = n; }
  public void run() {
    try {
      while (!isInterrupted()) {
        sleep((long)(java.lang.Math.random()*1000)); 
        buff.put(id);
      }
    } catch (InterruptedException e) { return; }
  }
}

/* ********** Consumer ********** */

class Consumer extends Thread {
  private Buffer buff;
  private int id;
  public Consumer(Buffer b, int n) { buff = b; id = n; }
  public void run() {
    try {
      while (!isInterrupted()) {
        sleep((long)(java.lang.Math.random()*1000)); 
        int k = buff.get();
        System.out.println("consumer " + id + " has retrieved the datum put by producer " + k);
      }
    } catch (InterruptedException e) { return; }
  }
}

/* *********** Buffer ************ */

class Buffer {
  private int content;
  private boolean is_empty;
  public Buffer() { is_empty = true; }
  public synchronized void put(int k) 
    throws InterruptedException
  {    
    while (!is_empty) wait();
    content = k;
    is_empty = false;
    notifyAll();
  }
  public synchronized int get() 
    throws InterruptedException
  {
    while (is_empty) wait();
    is_empty = true;
    notifyAll();
    return content;
  }
}