Wednesday, June 18, 2014

[Concurrency - State Models & Java Programs] Ch 5: Monitors and Condition Synchronization

Monitors are language features for concurrent programming. A monitor encapsulates data, which can only be observed and modified by monitor access procedures. Only a single access procedure may be active at a time. An access procedure thus has mutually exclusive access to the data variables encapsulated in the monitor. Monitors should sound familiar since we have already seen the monitor concept in the last chapter, though explained using different terminology. An object satisfies the data access requirement of a monitor since it encapsulates data which, if declared private, can be accessed only by the object’s methods. These methods can be synchronized to provide mutually exclusive access. Thus, a monitor is simply represented in Java as a class that has synchronized methods.
Monitors support condition synchronization in addition to ensuring that access to the data they encapsulate is mutually exclusive. Condition synchronization, as the term suggests, permits a monitor to block threads until a particular condition holds, such as a count becoming non-zero, a buffer becoming empty or new input becoming available. This chapter describes how condition synchronization in monitors is modeled and how it is implemented in Java.

5.1 Condition Synchronization

We illustrate condition synchronization using a simple example. A controller is required for a car park, which only permits cars to enter when the car park is not full and, for consistency, does not permit cars to leave when there are no cars in the car park. A snapshot of our Java simulation of the car park is given in Figure 5.1. It depicts the situation in which the car park is full, the barrier is down and no further cars are permitted to enter. Car arrivals and departures are simulated by separate threads. In Figure 5.1, the departures thread has been stopped to allow the car park to become full. The arrivals thread is therefore blocked from further progress.

Figure 5.1: Car park display.

5.1.1 Car Park Model

The first step in modeling a system is to decide which events or actions are of interest. In the car park system, we can abstract details such as display panel rotation and the starting and stopping of the display threads. We thus omit the actions concerned with running, rotation, pausing and terminating threads that we modeled in section 3.2.1. Instead, we concern ourselves with only two actions: car arrival at the car park and car departure from the car park. These actions are named arrive and depart respectively. The next step is to identify the processes. These are the arrivals process, the departures process and the process that controls access to the car park. Both the arrivals process and the departures process are trivial. They attempt to generate, respectively, a sequence of arrival actions and a sequence of departure actions. The car park control must only permit arrival actions to occur when there is space in the car park and departures to occur when there are cars in the car park. This expresses the synchronization conditions that must be satisfied by the other processes when interacting with the car park.
The car park model is given in Figure 5.2. The CARPARKCONTROL process uses the indexed state SPACES to record the number of available spaces in the car park. The control requirements described above have been modeled using the FSP guarded action construct (see section 2.1.5). Thus in state SPACES[0], arrive actions are not accepted and in state SPACES[N], depart actions are not accepted.

Figure 5.2: Car park model.
The behavior of the car park system is depicted as an LTS in Figure 5.3. The LTS has been generated directly from the model of Figure 5.2. It clearly shows that a maximum of four arrive actions can be accepted before a depart action must occur.

Figure 5.3: Car park LTS.

5.1.2 Car Park Program

Our models of concurrent systems represent all the entities in a system as processes. In implementing the behavior of a model as a Java program, we must decide which entities are active and which are passive. By active, we mean an entity that initiates actions; this is implemented as a thread. By passive, we mean an entity that responds to actions; this is implemented as a monitor. As we will see in subsequent examples, the decision as to which processes in a model become threads in the implementation and which become monitors is not always clear-cut. However, in the car park example, the decision is clear. The processes ARRIVALS and DEPARTURES, which initiate arrive and depart actions, should be implemented as threads. The CARPARKCONTROL process, which responds to arrive and depart actions, should be a monitor. The class structure of the car park program is depicted in Figure 5.4.

Figure 5.4: Car park class diagram.

We have omitted the DisplayThread and GraphicCanvas threads managed by ThreadPanel to simplify Figure 5.4. These are organized in exactly the same way as depicted in the class diagram for ThreadDemo in Chapter 3. The classes that are relevant to the concurrent execution of the program are the two Runnable classes, Arrivals and Departures, and the CarParkControl class, which controls arrivals and departures. Instances of these classes are created by the CarPark applet start() method:
public void start() {
  CarParkControl c =
     new DisplayCarPark(carDisplay,Places);
     arrivals.start(new Arrivals(c));
     departures.start(new Departures(c));
}
Arrivals and Departures are instances of the ThreadPanel class and carDisplay is an instance of CarParkCanvas as shown in the class diagram.
The code for the Arrivals and Departures classes is listed in Program 5.1. These classes use a ThreadPanel.rotate() method which takes as its parameter the number of degrees the rotating display segment is moved. The CarParkControl class must block the activation of arrive() by the arrivals thread if the car park is full and block the activation of depart() by the departures thread if the car park is empty. How do we implement this in Java?
Program 5.1: Arrivals and Departures classes.
Image from book
class Arrivals implements Runnable {
  CarParkControl carpark;

  Arrivals(CarParkControl c) {carpark = c;}

  public void run() {
    try {
      while(true) {
        ThreadPanel.rotate(330);
        carpark.arrive();
        ThreadPanel.rotate(30);
      }
    } catch (InterruptedException e){}
  }
}

class Departures implements Runnable {
  CarParkControl carpark;

  Departures(CarParkControl c) {carpark = c;}

  public void run() {
    try {
      while(true) {
        ThreadPanel.rotate(180);
        carpark.depart();
        ThreadPanel.rotate(180);
      }
    } catch (InterruptedException e){}
  }
}
Image from book

5.1.3 Condition Synchronization in Java

Java provides a thread wait set per monitor; actually, per object, since any object may have a monitor synchronization lock associated with it. The following methods are provided by class Object from which all other classes are derived.
public finalvoid notify()
Wakes up a single thread that is waiting on this object’s wait set.
public finalvoid notifyAll()
Wakes up all threads that are waiting on this object’s wait set.
public finalvoid wait() throwsInterruptedException
Waits to be notified by another thread. The waiting thread releases the synchronization lock associated with the monitor. When notified, the thread must wait to reacquire the monitor before resuming execution.
The operations fail if called by a thread that does not currently “own” the monitor (i.e. one that has not previously acquired the synchronization lock by executing a synchronized method or statement). We refer to a thread entering a monitor when it acquires the mutual exclusion lock associated with the monitor and exiting the monitor when it releases the lock. From the above definitions, it can be seen that a thread calling wait() exits the monitor. This allows other threads to enter the monitor and, when the appropriate condition is satisfied, to call notify() or notifyAll() to awake waiting threads. The operation of wait() and notify() is depicted in Figure 5.5.

Figure 5.5: Monitor wait() and notify().
The basic format for modeling a guarded action for some condition cond and action act using FSP is shown below:
FSP: when cond act -> NEWSTAT
The corresponding format for implementing the guarded action for condition cond and action act using Java is as follows:
Java: public synchronized void act()
             throws InterruptedException
      {
        while (!cond) wait();
        // modify monitor data
        notifyAll()
      }
The while loop is necessary to ensure that cond is indeed satisfied when a thread re-enters the monitor. Although the thread invoking wait() may have been notified that cond is satisfied, thereby releasing it from the monitor wait set, cond may be invalidated by another thread that runs between the time that the waiting thread is awakened and the time it re-enters the monitor (by acquiring the lock).
If an action modifies the data of the monitor, it can call notifyAll() to awaken all other threads that may be waiting for a particular condition to hold with respect to this data. If it is not certain that only a single thread needs to be awakened, it is safer to call notifyAll() than notify() to make sure that threads are not kept waiting unnecessarily.
Returning to the car park example, the implementation of the CarParkControl monitor is given in Program 5.2. Since either the ARRIVALS thread is blocked waiting space or the DEPARTURES thread is blocked waiting cars and these conditions are exclusive, only a single thread may be waiting on the monitor queue at any one time. Consequently, we can use notify() rather than notifyAll(). Note that we have made the spaces and capacity variables protected rather than private so that they can be accessed by the display class that is derived from CarParkControl.
Program 5.2: CarParkControl monitor.
Image from book
class CarParkControl {
  protected int spaces;
  protected int capacity;

  CarParkControl(n)
    {capacity = spaces = n;}

  synchronized void arrive()
      throws InterruptedException {
    while (spaces==0) wait();
    --spaces;
    notify();
  }

  synchronized void depart()
     throws InterruptedException{
    while (spaces==capacity) wait();
    ++spaces;
    notify();
  }
}
Image from book
The general rules for guiding the translation of the process model into a Java monitor are as follows:
Each guarded action in the model of a monitor is implemented as a synchronized method which uses a while loop and wait() to implement the guard. The while loop condition is the negation of the model guard condition.
and
Changes in the state of the monitor are signaled to waiting threads using notify() or notifyAll().
Thus in the car park model:
FSP: when(i>0) arrive->SPACES[i-1]
becomes
Java: while (spaces==0) wait(); --spaces;
and
FSP: when(i<N) depart->SPACES[i+1]
becomes
Java: while (spaces==N) wait(); ++spaces;
The state of the car park monitor is the integer variable spaces. Each method modifies spaces and consequently, the change is signaled by notify() at the end of each method.

5.2 Semaphores

Semaphores, introduced by Dijkstra (1968a), were one of the first mechanisms proposed to deal with inter-process synchronization problems. A semaphore s is an integer variable that can take only non-negative values. Once s has been given an initial value, the only operations permitted on s are up(s) and down(s) defined as follows:
down(s): when s>0 do decrement s;

up(s): increment s
In Dijkstra’s original proposal, the down operation was called P (for the first letter in the Dutch word passeren, which means “to pass”). The up operation was called V (for the first letter of the Dutch word vrijgeven, which means “to release”). Semaphores are implemented in the kernels of many operating systems and real-time executives. The above definition of semaphores seems to imply some sort of busy wait by down(s) until s becomes non-zero. In fact, in the kernel of an operating system, semaphores are usually implemented by a blocking wait as shown below:
down(s): if s>0 then
                            decrement s
                else
                            block execution of the calling process

up(s):       if processes blocked on s then
                            awaken one of them
                else
                            increment s
Implementations of semaphores usually manage processes, blocked on a semaphore by down, as a first-in-first-out (FIFO) queue. The up operation awakens the process at the head of the queue. FIFO queuing should not be relied on in reasoning about the correctness of semaphore programs.
In the following, we describe how semaphores are modeled and how they can be implemented using Java monitors. However, it should be realized that semaphores are a low-level mechanism sometimes used in implementing the higher-level monitor construct, rather than vice versa as presented here for pedagogic reasons.

5.2.1 Modeling Semaphores

The models of concurrent systems that we can describe in FSP are finite, to ensure they can be analyzed. Consequently, we can only model semaphores that take a finite range of values. If the range is exceeded then this is regarded as an error in the model as described below:
const Max = 3
range Int = 0..Max

SEMAPHORE(N=0) = SEMA[N],
SEMA[v:Int]    = (up->SEMA[v+1]
                 |when(v>0) down->SEMA[v-1]
                 ),
SEMA[Max+1]    = ERROR.
The behavior of the semaphore is depicted in Figure 5.6, with the ERROR state indicated by state(–1). In fact, since the FSP compiler automatically maps undefined states to the ERROR state, we can omit the last line of the description and model a semaphore more succinctly as:

Figure 5.6: Semaphore LTS.

SEMAPHORE(N=0) = SEMA[N],
SEMA[v:Int]    = (up->SEMA[v+1]
                 |when(v>0) down->SEMA[v-1]
                 ).
The model follows directly from the first definition for a semaphore in the previous section. The action down is only accepted when the value v of the SEMAPHORE is greater than zero. The action up is not guarded. SEMAPHORE can take values in the range 0..Max and has an initial value N. If an up action causes Max to be exceeded then SEMAPHORE moves to the ERROR state. When SEMAPHORE is used in a larger model, we must ensure that this ERROR state does not occur. As an example, we model the use of semaphores to provide mutual exclusion.
Figure 5.7 depicts a model in which three processes p[1..3] use a shared semaphore mutex to ensure mutually exclusive access to some resource. Each process performs the action mutex.down to get exclusive access and mutex.up to release it. Access to the resource is modeled as the action critical (for the critical section of code used to access the shared resource). The model for each of the processes is as shown below:
LOOP = (mutex.down->critical->mutex.up->LOOP).

Figure 5.7: Semaphore mutual exclusion model.
The composite process SEMADEMO, which combines processes and semaphore and which is depicted graphically in Figure 5.7, is defined as:
||SEMADEMO = (p[1..3]:LOOP
             ||{p[1..3]}::mutex:SEMAPHORE(1)).
Note that for mutual exclusion, the semaphore must be given the initial value one. The first process that tries to execute its critical action, performs a mutex.down action making the value of mutex zero. No further process can perform mutex.down until the original process releases exclusion by mutex.up. This can be seen clearly from the SEMADEMO labeled transition system in Figure 5.8.

Figure 5.8: SEMADEMO LTS.
It should also be clear from Figure 5.8 that no ERROR state is reachable in SEMADEMO since it does not appear in the LTS. In fact, the value of the semaphore does not exceed one (from the LTS, we can see that a trace of two consecutive mutex.up actions without an intermediate mutex.down cannot occur). For mutual exclusion, it is sufficient to use a binary semaphore which takes the values 0 or 1. We have already seen in the previous chapter that we can use the analysis tool to check mechanically for errors. We will see in subsequent chapters that we do not have to rely on visual inspection of the LTS to assert properties concerning sequences of actions. Of course, in this example, we can quickly check if mutex ever goes above 1 by setting Max to 1 and searching for errors.

5.2.2 Semaphores in Java

Semaphores are passive objects that react to up and down actions; they do not initiate actions. Consequently, we implement a semaphore in Java as a monitor class. The actions up and down become synchronized methods. The guard on the down action in the model is implemented using condition synchronization as we saw in section 5.1. The class that implements semaphores is listed in Program 5.3.
Program 5.3: Semaphore class.
Image from book
public class Semaphore {
  private int value;

  public Semaphore (int initial)
    {value = initial;}

  synchronized public void up() {
     ++value;
     notify();
  }

  synchronized public void down()
      throws InterruptedException {
    while (value== 0) wait();
    --value;
  }
}
Image from book

Even though the down() method in Program 5.3 changes the state of the monitor by decrementing value, we do not use notify() to signal the change in state. This is because threads only wait for the value of the semaphore to be incremented, they do not wait for the value to be decremented. The semaphore implementation does not check for overflow on increment. This is usually the case in semaphores implemented by operating systems. It is the responsibility of the programmer to ensure, during design, that overflow cannot occur. We advocate the use of analyzable models to check such properties.
Figure 5.9 depicts the display of the semaphore demonstration program modeled in the previous section. A thread executing in its critical section is indicated by a lighter-colored segment. Each thread display rotates counter-clockwise. In Figure 5.9, Thread 1 is executing in its critical section, Thread 2 is blocked waiting to enter its critical section and Thread 3 has finished its critical section and is executing non-critical actions. The sliders underneath each thread adjust the time a thread spends executing critical, as opposed to non-critical, actions. If the total time spent in critical sections by all three threads is less than a full rotation then it is possible to get all three threads to execute concurrently. In other words, there need be no conflict for access to the critical resource. This is often the case in real systems. Mechanisms for mutual exclusion only take effect when there is conflicting access to a shared resource. In real systems, it is therefore advisable to keep the time spent in critical sections as short as possible so as to reduce the likelihood of conflict.

Figure 5.9: SEMADEMO display.
The program behind the display of Figure 5.9 uses the same ThreadPanel class as before; however, it uses a different constructor to create the display with multicolored segments. The interface offered by the class, extended with the methods used in this chapter, is shown in Program 5.4.
Program 5.4: Extended version of ThreadPanel class.
Image from book
public class ThreadPanel extends Panel {

  // construct display with title and rotating arc color c
  public ThreadPanel(String title, Color c) {...}

  // hasSlider == true creates panel with slider
  public ThreadPanel
  (String title, Color c, boolean hasSlider) {...}

  // rotate display of currently running thread 6 degrees
  // return false when in initial color
  // return true when in second color
  public static boolean rotate()
         throws InterruptedException {...}

  // rotate display of currently running thread by degrees
  public static void rotate(int degrees)
         throws InterruptedException {...}

  // create a new thread with target r and start it running
  public void start(Runnable r) {...}

  // stop the thread using Thread.interrupt()
  public void stop() {...}
}
Image from book

The MutexLoop class, which provides the run() method for each thread, is listed in Program 5.5. The critical (mutually exclusive) actions are the rotate() actions which are executed when the segment changes to the lighter color. This is indicated by the rotate() method returning false when the rotating arc is dark-colored and true when light-colored.
Program 5.5: MutexLoop class.
Image from book
class MutexLoop implements Runnable {
  Semaphore mutex;

  MutexLoop (Semaphore sema) {mutex=sema;}

  public void run() {
    try {
      while(true) {
        while(!ThreadPanel.rotate());
        mutex.down(); // get mutual exclusion
        while(ThreadPanel.rotate()); //critical actions
        mutex.up(); //release mutual exclusion
      }
    } catch(InterruptedException e){}
  }
}
Image from book
The threads and semaphore are created in the usual way by the applet start() method:
public void start() {
     Semaphore mutex =
        new DisplaySemaphore(semaDisplay,1);
     thread1.start(new MutexLoop(mutex));
     thread2.start(new MutexLoop(mutex));
     thread3.start(new MutexLoop(mutex));
}
where thread1, thread2 and thread3 are ThreadPanel instances and semaDisplay is an instance of NumberCanvas.

5.3 Bounded Buffers

Buffers are frequently used in concurrent systems to smooth out information transfer rates between the producers of data and the consumers of that data. Consider, for example, a keyboard device driver that is supplying characters typed at a keyboard to an editor program. The editor can consume characters at a much faster rate, on average, than a person can type at a keyboard. However, some characters can take longer than others to process, for example a character that causes the screen to scroll or a keyboard command that invokes a formatting command. When the editor is processing a character that takes a long time to process, it is necessary to buffer characters from the keyboard, otherwise they would be lost. This buffer is sometimes referred to as the type-ahead buffer. It is an example of the sort of buffer that we describe in the following.
In this section we model and program a bounded buffer, which consists of a fixed number of slots. Items are put into the buffer by a producer process and removed by a consumer process. The buffer is organized so that the first item put into it will be the first item out (FIFO).
Figure 5.10 depicts the display of our example system in which a producer process communicates characters to a consumer process via a five-slot buffer. The small circle above the buffer indicates the next free slot into which the producer process can place a character. The circle below the buffer indicates the next slot from which the consumer process can take a character. The reader may note the similarity between this example and the initial car park example in section 5.1. In fact, the synchronization requirements are the same. The producer is only allowed to put a character into the buffer when there is a free slot and the consumer process can only get a character when there is at least one in the buffer. These are exactly the requirements for the car park, if we substitute space for slot and car for character. What is different between the two examples is the FIFO discipline enforced by the buffer, in contrast to the car park where cars can occupy any free space and need not leave in arrival order.

Figure 5.10: Bounded buffer display.

5.3.1 Bounded Buffer Model

The producer – consumer system with a bounded buffer is an example of a program that handles data items without altering them. In addition, the behavior of the producer, the consumer and the buffer itself are not affected by the value of the items they handle. In other words, they do not test the value of these data items. The behavior is said to be data-independent. If data independence can be established then models can be simplified by omitting the detailed representation of parameters and data structures. This leads to much smaller and more tractable models. The get and put operations in Figure 5.11 are simple actions that do not have parameters. The LTS for the bounded buffer system, depicted in Figure 5.12, should be compared with the car park LTS of Figure 5.3 to see the similarity between the synchronization behavior of the two systems.

Figure 5.11: Bounded buffer model.

Figure 5.12: Bounded buffer LTS.

5.3.2 Bounded Buffer Program

The BUFFER of the model becomes a monitor in the Java implementation, with synchronized methods put and get (Program 5.6). We have separated the interface of the buffer from its implementation since we will provide an alternative implementation in the next section.
Program 5.6: Buffer interface and BufferImpl class.
Image from book
public interface Buffer<E> {
  public void put(E o)
    throws InterruptedException; //put object into buffer
  public E get()
    throws InterruptedException; //get object from buffer
}

public class BufferImpl<E> implements Buffer<E> {
  protected E[] buf;
  protected int in = 0;
  protected int out= 0;
  protected int count= 0;
  protected int size;

  public BufferImpl(int size) {
    this.size = size;
    buf = (E[])new Object[size];
  }

  public synchronized void put(E o)
            throws InterruptedException {
    while (count==size) wait();
    buf[in] = o;
    ++count;
    in=(in+1) % size;
    notifyAll();
  }

  public synchronized E get()
           throws InterruptedException {
    while (count==0) wait();
    E o = buf[out];
    buf[out]=null;
    --count;
    out=(out+1) % size;
    notifyAll();
    return (o);
  }
}
Image from book
The buffer has been implemented as a general-purpose class that can buffer any type of Java object. The buffer data structure is a fixed size array buf, indexed by in which points to the next free slot and out which points to the next slot to be emptied. These indexes are incremented modulo the size of the buffer. The code for the producer and consumer programs is listed in Program 5.7.
Program 5.7: Producer and Consumer classes.
Image from book
class Producer implements Runnable {
  Buffer<Character> buf;
  String alphabet= "abcdefghijklmnopqrstuvwxyz";

  Producer(Buffer<Character> b) {buf = b;}

  public void run() {
    try {
      int ai = 0;
      while(true) {
        ThreadPanel.rotate(12);
        buf.put(alphabet.charAt(ai));
        ai=(ai+1) % alphabet.length();
        ThreadPanel.rotate(348);
      }
    } catch (InterruptedException e){}
  }
}

class Consumer implements Runnable {
  Buffer<Character> buf;

  Consumer(Buffer<Character> b) {buf = b;}

  public void run() {
    try {
      while(true) {
        ThreadPanel.rotate(180);
        Character c = buf.get();
        ThreadPanel.rotate(180);
      }
    } catch(InterruptedException e ){}
  }
}

5.4 Nested Monitors

Suppose that we did not wish to use condition synchronization directly in the implementation of the buffer monitor class but instead we decided to use two semaphores full and empty to reflect the state of the buffer. The semaphore empty counts the number of spaces and is decremented during a put operation. The put is, of course, blocked if the value of empty is zero. Similarly, full counts the number of items in the buffer and is decremented by a get operation. The get is therefore blocked if the value of full is zero. The modified buffer class is shown in Program 5.8.
Program 5.8: Buffer class using semaphores.
Image from book
class SemaBuffer<E> implements Buffer<E> {
  protected E[] buf;
  protected int in = 0;
  protected int out= 0;
  protected int count= 0;
  protected int size;

  Semaphore full; //counts number of items
  Semaphore empty; //counts number of spaces

  SemaBuffer(int size) {
    this.size = size; buf = (E[])new Object[size];
    full = new Semaphore(0);
    empty= new Semaphore(size);
  }

  synchronized public void put(E o)
              throws InterruptedException {
    empty.down();
    buf[in] = o;
    ++count;
    in=(in+1) % size;
    full.up();
  }

  synchronized public E get()
               throws InterruptedException{
    full.down();
    E o =buf[out];
    buf[out]=null;
    --count;
    out=(out+1) % size;
    empty.up();
    return (o);
  }
}
Image from book

The semaphores of Program 5.8 replace the count variable in the original implementation, the conditional waits on the value of count and the notification of changes in its value. An updated model to reflect the changes in the buffer implementation is shown below:
const Max = 5
range Int = 0..Max

SEMAPHORE(I=0) = SEMA[I],
SEMA[v:Int]    = (up->SEMA[v+1]
                 |when(v>0) down->SEMA[v-1]
                 ).

BUFFER = (put -> empty.down ->full.up ->BUFFER
         |get -> full.down ->empty.up ->BUFFER
         ).

PRODUCER = (put -> PRODUCER).
CONSUMER = (get -> CONSUMER).

||BOUNDEDBUFFER = (PRODUCER|| BUFFER || CONSUMER
                  ||empty:SEMAPHORE(5)
                  ||full:SEMAPHORE(0))@{put,get}.
A problem occurs when we check this model using the analyzer tool LTSA and find that it reports a potential deadlock together with a trace of actions to that deadlock:
Composing
 potential DEADLOCK
States Composed: 28 Transitions: 32 in 60ms
Trace to DEADLOCK:
     get
We discuss deadlock in more detail in the next chapter. However, in essence, it means that a system can make no further progress since there are no further actions it can take. The deadlock in the model can be seen in the demonstration version of the program by starting the consumer and letting it block, waiting to get a character from the empty buffer. When the producer is started, it cannot put a character into the buffer. Why? The reason is to do with the use of two levels of synchronization lock: the first gives mutually exclusive access to the buffer monitor and the second gives mutually exclusive access to the semaphores.
When the consumer calls get, it acquires the Buffer monitor lock and then acquires the monitor lock for the full semaphore by calling full.down() to check if there is something in the buffer. Since initially the buffer is empty, the call to full.down() blocks the consumer thread (using wait()) and releases the monitor lock for the full semaphore. However, it does not release the monitor lock for Buffer. Consequently, the producer cannot enter the monitor to put a character into the buffer and so no progress can be made by either producer or consumer – hence the deadlock. The situation described above is known as the nested monitor problem. The only way to avoid it in Java is by careful design. In our example, the deadlock can be removed by ensuring that the monitor lock for the buffer is not acquired until after semaphores are decremented (Program 5.9).
Program 5.9: Fixed bounded buffer using semaphores.
Image from book
class FixedSemaBuffer<E> implements Buffer<E> {
  protected E[] buf;
  protected int in = 0;
  protected int out= 0;
  protected int count= 0; //only used for display purposes
  protected int size;

  Semaphore full;  //counts number of items
  Semaphore empty; //counts number of spaces

  FixedSemaBuffer(int size) {
    this.size = size; buf = (E[])new Object[size];
    full = new Semaphore(0);
    empty= new Semaphore(size);
  }

  public void put(E o)
             throws InterruptedException {
    empty.down();
    synchronized(this){
      buf[in] = o; ++count; in=(in+1)%size;
    }
    full.up();
  }

  public E get()
          throws InterruptedException{
    full.down(); E o;
    synchronized(this){
      o =buf[out]; buf[out]=null;
      --count; out=(out+1)%size;
    }
    empty.up();
    return (o);
  }
}
Image from book

As mentioned before, in this book we advocate the use of the model and analysis to aid in the process of “careful design”. Those parts of the model which need to be revised to take into account the changes to the buffer, documented in Program 5.9, are shown below:
BUFFER = (put -> BUFFER
         |get -> BUFFER
         ).

PRODUCER = (empty.down->put ->full.up ->PRODUCER).
CONSUMER = (full.down ->get ->empty.up->CONSUMER).
Moving the semaphore actions from the buffer process to the producer and consumer processes reflects the change in the implementation where the semaphore actions are performed outside the monitor (i.e. before acquiring the monitor lock). If this modified model is composed and minimized, it generates an identical LTS to that depicted in Figure 5.12 for the original model. This gives us confidence that our revised semaphore implementation of the bounded buffer is equivalent to the original one which used wait() and notify() directly.

5.5 Monitor Invariants

An invariant for a monitor is an assertion concerning the variables it encapsulates. This assertion must hold whenever there is no thread executing inside the monitor. Consequently, the invariant must be true at any point that a thread releases the monitor lock – when a thread returns from a synchronized method call and when a thread is blocked by a wait(). A formal proof of the correctness of a monitor can be achieved by demonstrating that the constructor for a monitor establishes the invariant and that the invariant holds after the execution of each access method and just before a wait() is executed. Such an approach requires a programming logic, a formal logical system that facilitates making precise statements about program execution. Greg Andrews (1991) uses this approach in his book. Similarly, Fred Schneider (1997) discusses formal derivation and reasoning about concurrent programs in his book.
Instead, for the reasons outlined at length in Chapter 1, we have chosen to use a model-based approach amenable to mechanical proof. The disadvantage is that our mechanical proofs only apply to specific cases or models while the manual proof-theoretic approach used by Andrews permits correctness proofs for the general case. For example, a proof method could establish monitor correctness for all sizes of a bounded buffer rather than just a specific size. However, it is usually the case that if a model for a specific case is shown to be correct, the general case can be inferred by induction.
Although we do not use invariants in formal correctness proofs, they are useful program documentation that aid informal correctness reasoning. The invariants for the monitor programs developed in this chapter are:
CarParkControl Invariant: 0  spaces  N
The invariant for the car park controller simply states that the number of spaces available must always be greater than or equal to zero and less than or equal to the maximum size of the car park (N).
Semaphore Invariant: 0  value
The semaphore invariant simply asserts that the value of a semaphore must always be a non-negative value.
Buffer Invariant: 0  count  size
              and 0  in < size
              and 0  out < size
              and in = (out + count) modulo size
The bounded buffer invariant asserts that the number of items in the buffer must lie in the range zero to size and that the indexes in and out must lie in the range zero to size-1. It states that the in index must always be count items “ahead” of the out index where ahead means addition modulo size.
Invariants are also used to reason about the correctness of classes in sequential object-oriented programs. The invariant is required to hold after each method execution. The difference in concurrent programs is the additional responsibility to establish that the invariant holds at any point where the object’s monitor lock is released. These additional points are where a wait() can be executed.

Summary

In this chapter, we introduced condition synchronization, which in combination with the mutual exclusion provided by synchronized methods, supports the concept of a monitor in Java. Condition synchronization is implemented using the wait(), notify() and notifyAll() primitives which operate on a waiting queue which can be associated with any Java object. Operation wait() suspends the calling thread on the wait queue, notify() unblocks one of the threads on the wait queue and notifyAll() unblocks all the threads on the wait queue. When a thread suspends itself by calling wait(), the monitor mutual exclusion lock is released. Use of these primitives causes an exception if the invoking thread does not currently hold the monitor lock.
Model processes that react to actions rather than instigate them are usually translated into monitors in the program that implements the model. Each guarded action in the model of a monitor is implemented as a synchronized method which uses a while loop and wait() to implement the guard. The while loop condition is the negation of the model guard condition. Changes in the state of the monitor are signaled to waiting threads using notify() or notifyAll().
Nested monitor calls should be used with great care as they can cause a program to deadlock. This can occur since a thread that waits in a monitor releases only its lock, not the lock of any monitor from which it may have been called.

Notes and Further Reading

The idea of associating data encapsulation with mutual exclusion, which is the essence of the monitor concept, is jointly due to Edsger W. Dijkstra (1972b), Per Brinch-Hansen (1972) and C.A.R. Hoare (1974). The monitors in C.A.R. Hoare’s classic paper differ in a number of respects from the way monitors appear in Java. Condition wait queues are declared explicitly in the original proposal and more than one can be declared in a monitor. This contrasts with the Java monitor, which permits only a single implicit condition queue. Multiple queues allow less rescheduling and thread-awakening if multiple threads are waiting on different conditions. In Java, all threads must be awakened to re-test their waiting conditions. If a thread’s condition does not hold, it blocks again. In practice, threads waiting on different conditions usually wait at different times and consequently there is no extra thread-activation cost. Even when it does occur, the extra scheduling does not usually cause a problem.
Another difference is the semantics of notify(). The Java semantic for notify() is known as signal and continue. This means that the notified thread is taken off the wait queue and put into the scheduler’s ready queue. However, the thread invoking the notify operation does not necessarily give up the processor and can continue running. Conditions in Java are thus always re-tested when the notified thread regains the monitor, since the condition may have been invalidated between the time it was notified and the time the monitor lock is re-acquired. In contrast, the notify operation of the original proposal had signal and urgent wait semantics. Notify would cause the notified thread to be executed immediately and the notifying thread to be suspended. However, the notifying thread would regain the monitor lock before new entries. Signal and urgent wait has the advantage that wait conditions do not need to be re-tested. The disadvantages are additional implementation complexity and, in a Java context, the fact that the semantics do not fit well with having a single wait queue. An extensive discussion of the different semantics possible for condition synchronization may be found in Greg Andrews’ book (1991).
The development of monitors was inspired by the class concept of SIMULA-67 (Birtwistle, Dahl, Myhrhaug, et al., 1973). Monitors have been included in a wide range of early concurrent programming languages. Concurrent Pascal (Brinch-Hansen, 1975) was the first concurrent programming language to include monitors. Subsequent influential concurrent programming languages with monitors include Modula (Wirth, 1977), Mesa (Lampson and Redell, 1980), Pascal Plus (Welsh and Bustard, 1979), Concurrent Euclid (Holt, 1983) and Turing Plus (Holt and Cordy, 1988). The problem of nested monitor calls was raised by Andrew Lister (1977). Java has brought the development of monitors full circle by including the concept in an object-oriented programming language.

Exercises

  • 5.1 A single-slot buffer may be modeled by:
    ONEBUF = (put->get->ONEBUF).
  • Program a Java class, OneBuf, that implements this one-slot buffer as a monitor.
  • 5.2 Replace the condition synchronization in your implementation of the one-slot buffer by using semaphores. Given that Java defines assignment to scalar types (with the exception of long and double) and reference types to be atomic, does your revised implementation require the use of the monitor’s mutual exclusion lock?
  • 5.3 In the museum example (Chapter 3, exercise 3.6), identify which of the processes, EAST, WEST, CONTROL and DIRECTOR, should be threads and which should be monitors. Provide an implementation of the monitors.
  • 5.4 The Dining Savages: A tribe of savages eats communal dinners from a large pot that can hold M servings of stewed missionary. When a savage wants to eat, he helps himself from the pot unless it is empty in which case he waits for the pot to be filled. If the pot is empty, the cook refills the pot with M servings. The behavior of the savages and the cook is described by:
    SAVAGE = (getserving -> SAVAGE).
    COOK   = (fillpot -> COOK).
  • Model the behavior of the pot as an FSP process and then implement it as a Java monitor.
  • 5.5 FSP allows multiple processes to synchronize on a single action. A set of processes with the action sync in their alphabets must all perform this action before any of them can proceed. Implement a monitor called Barrier in Java with a sync method that ensures that all of N threads must call sync before any of them can proceed.
  • 5.6 The Savings Account Problem: A savings account is shared by several people. Each person may deposit or withdraw funds from the account subject to the constraint that the balance of the account must never become negative. Develop a model for the problem and from the model derive a Java implementation of a monitor for the savings account.

Tuesday, June 17, 2014

[Concurrency - State Models & Java Programs] Ch 4: Shared Objects and Mutual Exclusion

In the last chapter, we discussed the execution of multiple processes on one or more processors, modeling concurrent execution by interleaving and executing multiple concurrent threads in a Java program. We explained how process interaction is modeled using shared atomic actions, but not how real processes or threads interact. In this chapter, we turn to the issues involved in constructing concurrent programs in which threads interact to communicate and cooperate.
The simplest way for two or more threads in a Java program to interact is via an object whose methods can be invoked by the set of threads. This shared object’s state can of course be observed and modified by its methods. Consequently, two threads can communicate by one thread writing the state of the shared object and the other thread reading that state. Similarly, a set of threads may cooperate to update some information encapsulated in a shared object. Unfortunately, as we will explain, this simple scheme of interaction does not work.

4.1 Interference

We have seen that the execution of the instructions from a set of threads can be interleaved in an arbitrary fashion. This interleaving can result in incorrect updates to the state of a shared object. The phenomenon is known as interference. The problem of interference, and how to deal with it, is the main topic of this chapter.

4.1.1 Ornamental Garden Problem

To focus on the issues of thread interaction, we use an example known as the problem of the Ornamental Garden, due to Alan Burns and Geoff Davies (1993). The problem is stated as follows. A large ornamental garden is open to members of the public who can enter through either of two turnstiles as depicted in Figure 4.1. The management wants to determine how many people there are in the garden at any one time. They require a computer system to provide this information.
To simplify the problem further, we consider a garden that people are allowed to enter but never leave! The concurrent program to implement the population count required by the management of the ornamental garden consists of two concurrent threads and a shared counter object. Each thread controls a turnstile and increments the shared counter when a person passes through the turnstile. The class diagram for the program is depicted in Figure 4.2.
The Counter object and Turnstile threads are created by the go() method of the Garden applet shown below in which eastD, westD and counterD are objects of the same NumberCanvas class that we used in Chapter 2.
private void go() {
  counter = new Counter(counterD);
  west = new Turnstile(westD,counter);
  east = new Turnstile(eastD,counter);
  west.start();
  east.start();
}
The Turnstile thread shown in Program 4.1 simulates the periodic arrival of a visitor to the garden by sleeping for half a second and then invoking the increment() method of the counter. After the arrival of Garden.MAX visitors, the run() method exits and consequently, the thread terminates.
Program 4.1: Turnstile class.
Image from book
class Turnstile extends Thread {
  NumberCanvas display;
  Counter people;

  Turnstile(NumberCanvas n,Counter c)
    { display = n; people = c; }

  public void run() {
    try{
      display.setvalue(0);
      for (int i=1;i<=Garden.MAX;i++){
        Thread.sleep(500); //0.5 second between arrivals
        display.setvalue(i);
        people.increment();
      }
    } catch (InterruptedException e) {}
  }
}
Image from book
The remaining class Counter is more complex than is strictly necessary. The additional complexity is to ensure that the program demonstrates the effects of interference independently of any particular implementation of Java. To ensure that the program demonstrates the desired effect, Program 4.2 ensures that arbitrary interleaving occurs.
Program 4.2: Counter class.
Image from book
class Counter {
  int value=0;
  NumberCanvas display;

  Counter(NumberCanvas n) {
    display=n;
    display.setvalue(value);
  }

  void increment() {
    int temp = value;    //read value
    Simulate.HWinterrupt();
    value=temp+1;       //write value
    display.setvalue(value);
  }
}
Image from book
It does this by using the class Simulate which provides the method HWinterrupt(). The method, when called, sometimes causes a thread switch by calling Thread.yield() and sometimes omits the call leaving the current thread running. The idea is to simulate a hardware interrupt which can occur at arbitrary times between reading and writing to the shared Counter when performing an increment. Thus thread switches can occur at arbitrary times as discussed at the beginning of the last chapter. The Simulate class is defined by the following code:
class Simulate {
    public static void HWinterrupt() {
        if (Math.random()< 0.5) Thread.yield();
    }
}
The problem with the Ornamental Garden program is illustrated by the screen shot of the running applet in Figure 4.3. When the Go button is pressed, the Garden.go() method is invoked to create a Counter object and the two Turnstile threads. Each thread then increments the counter exactly Garden.MAX times and then terminates. The value of the constant Garden.MAX has been set to 20, consequently, when both Turnstile threads terminate, the counter display should register that 40 people have entered the garden. In fact, as can be seen from Figure 4.3, the counter registers only 31. Where have the missing people gone? Why have nine increments to the counter been lost? To investigate why, we develop a model of the Ornamental Garden problem.

Figure 4.3: Garden display.

4.1.2 Ornamental Garden Model

In the remainder of the book, we generally model each object or set of objects as an FSP process. However, to find out why the Ornamental Garden program operates incorrectly, we must model it at the level of store accesses. Consequently, the model includes a VAR process that describes the read and write accesses to a store location. This store location is the value variable encapsulated by the people instance of the Counter class (Program 4.2). The complete model is described in Figure 4.4. The reader may be surprised that there is no explicit mention of an increment action. Instead, increment is modeled using read and write actions by the definition INCREMENT inside TURNSTILE. Each thread object, east and west, has its own copy of the read and write actions that make up the increment operation or procedure. This models what happens in the actual Java program since methods are re-entrant and thus the instructions which constitute a method may be interleaved on behalf of the threads executing the method concurrently. In other words, method activations are not atomic actions. The LTS for the TURNSTILE is given in Figure 4.5.

The alphabet of the process VAR has been declared explicitly as the set in Figure 4.4. We have not used set constants before. A set constant can be used wherever we previously declared sets of action labels explicitly. Sets are simply a way of abbreviating model descriptions. VarAlpha is declared as follows:
set VarAlpha = {value.{read[T],write[T]} }
The alphabet for the TURNSTILE process is extended with this set using the alphabet extension construct +{...}. This is to ensure that there are no unintended free actions. For example, if a VAR write of a particular value is not shared with another process then it can occur autonomously. A TURNSTILE process never engages in the action value.write[0] since it always increments the value it reads. However, since this action is included in the alphabet extension of TURNSTILE, although it is not used in the process definition, it is prevented from occurring autonomously. The TURNSTILE process is slightly different from its Java implementation in that it does not run for a fixed number of arrivals but may end at any point. However, it cannot end in the middle of updating the shared variable value. The end action is only accepted as an alternative to an arrive action. Furthermore, TURNSTILE is defined as recursive so that analysis (discussed below) will not report spurious deadlocks as would be the case if we had used STOP after the action end. Note that the shared variable VAR is not only shared by the turnstiles east and west, but also by display which is used for checking purposes.
Having developed a model of the Ornamental Garden program, in some detail, what can we do with it? Well, we can animate the model using the LTSA tool to produce action traces for particular input scenarios. For example, the trace in Figure 4.6 illustrates the case where there is an east arrival and a west arrival and then end occurs.

The trace is correct in that after two arrivals the counter has a value of two. However, we might try many input scenarios before finding out what is wrong with the program. To automate the search for the error, we combine a TEST process with the existing model that signals when an erroneous action trace occurs. The process is defined below:
TEST       = TEST[0],
TEST[v:T]  =
     (when (v<N){east.arrive,west.arrive}->TEST[v+1]
     |end->CHECK[v]
     ),
CHECK[v:T] =
    (display.value.read[u:T] ->
       (when (u==v) right -> TEST[v]
       |when (u!=v) wrong -> ERROR
       )
    )+{display.VarAlpha}.
The process counts the total number of east.arrive and west.arrive actions. When an end action occurs, and consequently the shared variable updates are complete, it checks that the value stored is the same as the total number of arrival events. If not, an error is declared by moving into the ERROR state. ERROR (like STOP) is a predefined FSP local process (or state). It is always numbered -1 in the equivalent LTS. Again, alphabet extension is used to ensure that no actions prefixed by display can occur autonomously.
The TEST process is combined with the existing model as follows:
||TESTGARDEN = (GARDEN || TEST).
We can now request the LTSA analysis tool to perform an exhaustive search to see if the ERROR state in TEST can be reached and if so to produce an example trace. The trace produced is:
Trace to property violation in TEST:
     go
     east.arrive
     east.value.read.0
     west.arrive
     west.value.read.0
     east.value.write.1
     west.value.write.1
     end
     display.value.read.1
     wrong
This trace clearly indicates the problem with the original Java program. Increments are lost because the shared variable is not updated atomically. Thus both east and west turnstiles read the value 0 and write 1. If the east increment finished before the west increment started or vice versa, then the result would be two (as in the previous trace).
Destructive update, caused by the arbitrary interleaving of read and write actions, is termed interference.
In real concurrent programs, interference bugs are extremely difficult to locate. They occur infrequently, perhaps due to some specific combination of device interrupts and application I/O requests. They may not be found even after extensive testing. We had to include a simulated interrupt in the example program to demonstrate the error. Without the simulated interrupt, the program is still incorrect, although the erroneous behavior may not manifest itself on all systems.
The general solution to the problem of interference is to give methods that access a shared object mutually exclusive access to that object. This ensures that an update is not interrupted by concurrent updates. As we see in the following sections, methods with mutually exclusive access can be modeled as atomic actions.

4.2 Mutual Exclusion in Java

Concurrent activations of a method in Java can be made mutually exclusive by prefixing the method with the keyword synchronized.
The Counter class from the Ornamental Garden program can be corrected by deriving a SynchronizedCounter class from Counter and making the increment method in the subclass synchronized as shown in Program 4.3.
Program 4.3: Corrected Counter class.
Image from book
class SynchronizedCounter extends Counter {

  SynchronizedCounter(NumberCanvas n)
     {super(n);}

  synchronized void increment() {
       super.increment();
  }
}
Image from book
Java associates a lock with every object. The Java compiler inserts code to acquire the lock before executing the body of a synchronized method and code to release the lock before the method returns. Concurrent threads are blocked until the lock is released. Since only one thread at a time may hold the lock, only one thread may be executing the synchronized method. If this is the only method, as in the example, mutual exclusion to the shared object is ensured. If an object has more than one method, to ensure mutually exclusive access to the state of the object, all the methods should be synchronized.
Access to an object may also be made mutually exclusive by using the synchronized statement:
synchronized (object) { statements }
This acquires the referenced object’s lock before executing the bracketed statement block and releases it on exiting the block. For example, an alternative (but less elegant) way to correct the example would be to modify the Turnstile.run() method to use:
synchronized(people) {people.increment();}
This is less elegant as the user of the shared object has the responsibility for imposing the lock, rather than embedding it in the shared object itself. Since not all users of the object may act responsibly, it may also be less secure against interference.
The output from the corrected Ornamental Garden program is depicted in Figure 4.7. The only change is to use the class defined in Program 4.3 rather than the original Counter class. This change is made by clicking the Fix It check box before pressing Go.
Once a thread has acquired the lock on an object by executing a synchronized method, that method may itself call another synchronized method from the same object (directly or indirectly) without having to wait to acquire the lock again. The lock counts how many times it has been acquired by the same thread and does not allow another thread to access the object until there has been an equivalent number of releases. This locking strategy is sometimes termed recursive locking since it permits recursive synchronized methods. For example:
public synchronized void increment(int n) {
   if (n>0) {
       ++value;
       increment(n-1);
   } else return;
}
This is a rather unlikely recursive version of a method which increments value by n. If locking in Java was not recursive, it would cause a calling thread to be blocked forever, waiting to acquire a lock which it already holds!

4.3 Modeling Mutual Exclusion

The simplest way to correct the model of the Ornamental Garden program listed in Figure 4.4 is to add locking in exactly the same way as it was added to the Java program. For simplicity, we ignore the detail that Java locks are recursive since whether or not the lock is recursive has no impact on this problem. A (non-recursive) lock can be modeled by the process:
LOCK = (acquire->release->LOCK).
The composition LOCKVAR associates a lock with a variable. It is substituted for VAR in the definition of GARDEN.
||LOCKVAR = (LOCK || VAR).
The alphabet VarAlpha is modified as follows to include the additional locking actions:
set VarAlpha = {value.{read[T],write[T],
                       acquire, release}}
Finally, the definition of TURNSTILE must be modified to acquire the lock before accessing the variable and to release it afterwards:
TURNSTILE = (go    -> RUN),
RUN       = (arrive-> INCREMENT
            |end   -> TURNSTILE),
INCREMENT = (value.acquire
             -> value.read[x:T]->value.write[x+1]
             -> value.release->RUN
            )+VarAlpha.
We can check this model in exactly the same way as before using TEST. An exhaustive search does not find any errors. Consequently, we have mechanically verified that this new version of the model satisfies the property that the count value is equal to the total number of arrivals when stop is pressed. A sample execution trace of the new model is shown below:
go
east.arrive
east.value.acquire
east.value.read.0
east.value.write.1
east.value.release
west.arrive
west.value.acquire
west.value.read.1
west.value.write.2
west.value.release
end
display.value.read.2
right
Now that we have shown that we can make shared actions indivisible or atomic using locks, we can abstract the details of variables and locks and model shared objects directly in terms of their synchronized methods. We can perform abstraction mechanically by hiding actions. For example, we can describe the behavior of the SynchronizedCounter class (over a finite integer range) by:
const N = 4
range T = 0..N

VAR = VAR[0],
VAR[u:T] = ( read[u]->VAR[u]
           | write[v:T]->VAR[v]).

LOCK = (acquire->release->LOCK).

INCREMENT = (acquire->read[x:T]
             -> (when (x<N) write[x+1]
                 ->release->increment->INCREMENT
                )
             )+{read[T],write[T]}.

||COUNTER = (INCREMENT||LOCK||VAR)@{increment}.
The definition of INCREMENT has been slightly modified from that used previously. The when clause ensures that the increment action can only occur when the value stored is less than N. In other words, increment is not allowed to overflow the range T. The alphabet declaration @{increment} means that read, write, acquire and release become internal actions (tau) of COUNTER. The LTS which results from minimizing COUNTER is depicted in Figure 4.8.
We can describe a single process that generates exactly the same LTS:
COUNTER = COUNTER[0],
COUNTER[v:T] = (when (v<N) increment->COUNTER[v+1]).
This is a much more abstract and consequently simpler model of the shared Counter object with its synchronized increment method. We have demonstrated above (by LTSA minimization) that it has exactly the same observable behavior as the more complex definition. A display action to read the value of the counter can be added as shown below:
DISPLAY_COUNTER = COUNTER[0],
COUNTER[v:T] = (when (v<N) increment->COUNTER[v+1]
               |display[v] -> COUNTER[v]).
The LTS which results from minimizing DISPLAY_COUNTER is depicted in Figure 4.9.
To implement this action in the Java class, we would simply add the synchronized method:
public synchronized int display() {
     return value;
}
In the following chapters, we usually model shared objects at this level of abstraction, ignoring the details of locks and mutual exclusion (as provided by the use of synchronized methods in Java). Each shared object is modeled as an FSP process, in addition to modeling each Java thread as an FSP process. The model of a program does not distinguish active entities (threads) from passive entities (shared objects). They are both modeled as finite state machines. This uniform treatment facilitates analysis.

Summary

In this chapter, we have discussed thread interaction via shared objects. The Ornamental Garden example served to demonstrate that uncontrolled interleaving of method instructions leads to destructive update of the state of the shared object. This is termed interference. Interference can be avoided by giving each concurrent method activation mutually exclusive access to the shared state. In Java, this is achieved by making such methods synchronized. Synchronized methods acquire a lock associated with the object before accessing the object state and release the lock after access. Since only one thread at a time can acquire the lock, synchronized methods obtain mutually exclusive access to the object state.
Interference bugs in real concurrent programs are notoriously difficult to find. They can be found by analyzing program models as we demonstrated. However, this requires detailed modeling at the level of store accesses to variables. Such models quickly become too large to analyze. The answer is to ensure systematically that all the methods of objects shared between threads are synchronized. They can then be treated as atomic actions for modeling purposes.

Notes and Further Reading

We have dealt with the problem of access to shared objects encapsulating variables. However, access to any resource must be made mutually exclusive if the resource cannot be concurrently shared. These resources are sometimes termed serially reusable since they can be used by many processes at different times but not shared by many processes at the same time. For example, a printer can only produce the output from one print job at a time. The solution of using locks to ensure mutual exclusion is a general one that can be applied to controlling access to a printer in the same way as to shared variables.
A solution to the mutual exclusion problem was first proposed by Dijkstra (1965). All operating systems and concurrent programming textbooks deal with mutual exclusion. Usually, great emphasis is placed on the concept of a critical section. A critical section is simply the section of code belonging to a thread or process which accesses the shared variables. To ensure correct behavior, this critical section must be given mutually exclusive access by acquiring a lock before the critical section and releasing it afterwards. We have not used the term critical section since, in an object-oriented language such as Java, shared variables are encapsulated in objects and accessed via methods. In other words, synchronized methods are critical sections. The synchronized statement is another way of making a section of code a critical section in Java. However, it is usually the case in well-designed concurrent object-oriented programs that critical sections are methods.

Exercises

  • 4.1 Modify the uncorrected version of the Ornamental Garden program such that Turnstile threads can sleep for different times. Is it possible to choose these sleep times such that interference does not occur?
  • 4.2 Given the following declarations:
    const N = 3
    range P = 1..2 //thread identities
    range C = 0..N //counter range for lock
    
  • Model a Java recursive lock as the FSP process RECURSIVE_LOCK with the alphabet {acquire[p:P],release[p:P]}. The action acquire[p] acquires the lock for thread p.
  • 4.3 A central computer connected to remote terminals via communication links is used to automate seat reservations for a concert hall. A booking clerk can display the current state of reservations on the terminal screen. To book a seat, a client chooses a free seat and the clerk enters the number of the chosen seat at the terminal and issues a ticket. A system is required which avoids double-booking of the same seat while allowing clients free choice of the available seats. Construct a model of the system and demonstrate that your model does not permit double-bookings. (Hint: It is only necessary to model a few terminals and a few seats. Remember, a seat can appear to be free although it is booked or being booked by another clerk.)
  • 4.4 Write a Java program that implements the seat reservation system of exercise 4.3.