CSE 428: Lectures 15 and 16


Concurrent Programming in Java: Synchronization and Communication

We have seen that threads are autonomous sequential activities, but, in general, these activities are not independent. Usually, threads may need to interact with each otherfor various reasons, for instance In Java, the typical way in which two threads interact with each other is by operating on a common object. This is usually a third (separated) object and it does not need to be a thread.

When two threads operate on a common object there is the risk of a situation known as interference or race condition. We will see how to solve this problem in Java by using synchronization.

An example illustrating the problem of race condition

Assume that two brothers, Joe and John, share a common bank account. They both can, independently, read the balance, make a deposit, and withdraw some money.

We can model the bank account as an object of the following class:

   class Account {
       private double balance;

       public Account(double initialDeposit) {
           balance = initialDeposit;
       }

       public double getBalance() {
           return balance;
       }
      
       public void deposit(double amount) { 
           balance += amount;
      }   

       public void withdraw(double amount) { 
           if ( balance >= amount ) { balance -= amount; } 
      }    // no negative balance allowed

   }
We can model the account holders as two threads:
   class AccountHolder extends Thread {
       private Account acc;

       public AccountHolder(Account a) {
           acc = a;
       }
      
      public void run() {
          ...
          acc.withdraw(100);
          ...
      }
   }
The following fragment of code causes the creation of the account and of the two account holders:
   ...
   Account accnt = new Account(150);
   AccountHolder John = new AccountHolder(accnt).start();
   AccountHolder Joe  = new AccountHolder(accnt).start();
   ...
Note: accnt is a reference to the account, thus, although it is passed by value in AccountHolder(accnt), John and Joe will share the same account which accnt refers to. Namely, the changes made by John will be visible to Joe and viceversa.

There is a problem with this code: the activities of John and Joe can interfere in an odd way, and generate erroneous situations. This may happen for instance when they both try to do a withdraw "at the same time"

At the beginning: balance is 150

   John: accnt.withdraw(100)     Joe: accnt.withdraw(100) 
      
   balance >= 100 ? 
           |
           | yes                  balance >= 100 ?
           |                              |
           V                              | yes 
   balance = balance - 100                |
                                          V
                                  balance = balance - 100
At the end: balance is -50.

The problem derives from the fact that the scheduler may interleave the activities of John and Joe on the account in an arbitrary way. In the example, the activities are interleaved in such a way that both John and Joe do the test before the other does the withdraw. The consequence is a final (undesired) situation of a negative balance.

Perhaps more surprisingly, in the previous code there is a problem also with the deposit method.

       public void deposit(double amount) { 
           balance += amount;
      }   
In fact, although the instruction balance += amount looks "atomic", in the compiled code it is "broken" in a sequence of several, more elementary actions. Typically the balance is copied in an internal register, then the register is increased by the desired amount, and finally the register is copied back in the balance.
            b = balance;
            b += amount;
            balance = b;   
It should now be clear that also two deposits may create an erroneous situation:

At the beginning: balance is 150

   John: accnt.deposit(100)     Joe: accnt.deposit(100) 
      
   b = balance  (b = 150)
     |
     |                           b = balance  (b = 150)
     V                             |
   b += 100     (b = 250)          |
     |                             V
     |                           b += 100     (b = 250)
     V                             |
   balance = b                     |
                                   V
                                 balance = b
At the end: balance is 250 (instead of 350).

In general, there is a risk of race condition whenever two threads apply concurrently to the same object a method (or two different methods) operating on a common field, and at least one of the methods updates the field.

Note that there is a problem even if one of the methods updates the field and the other only reads the field. In general reading a field that is being updated by another thread might give unpredictable results.

There is no problem, on the contrary, with methods accessing common fields only in reading mode. Such read-only methods can (and should) run concurrently, thus optimizing execution time.

Solving the problem of race condition: mutual exclusion

A problematic sequence of actions, namely a sequence that we don't want to be interleaved with actions executed by other threads, is called critical section. The problem of interference can be solved by incapsulating the critical sections so that they will be executed "atomically", i.e. without interleaving. Sequences of actions incapsulated in this way are called mutually exclusive.
 
   John: accnt.deposit(100)        Joe: accnt.deposit(100) 
   
   +--------------------------+
   |                          |     delay until the critical section 
   | b = balance    (b = 150) |     executed by John is completed
   |   |                      |
   |   |                      |        ...
   |   V                      |                       
   | b += 100       (b = 250) | 
   |   |                      |
   |   |                      |       
   |   V                      |        ... 
   | balance = b              |  
   |                          |                                 
   +--------------------------+     John has terminated, Joe can start
				  +--------------------------+
				  |                          |
				  | b = balance    (b = 250) |
				  |   |                      |
				  |   |                      |     
				  |   V                      |                  
				  | b += 100       (b = 350) | 
				  |   |                      |
				  |   |                      | 
				  |   V                      | 
				  | balance = b              | 
				  |                          |                                 
                                  +--------------------------+

Mutual exclusion in Java

In Java, we can specify that we want a method to be executed atomically by declaring it synchronized. Synchronized methods are mutually exclusive, i.e. the actions of two threads executing synchonized methods on the same objects cannot be interleaved

Example: the correct definition of Account

   class Account {
       private double balance;

       public Account(double initialDeposit) {
           balance = initialDeposit;
       }

       public synchronized double getBalance() {
              ------------
           return balance;
       }
      
       public synchronized void deposit(double amount) { 
              ------------
           balance += amount;
      }   

       public synchronized void withdraw(double amount) { 
              ------------
           if ( balance >= amount ) { balance -= amount; } 
      }    // no negative balance allowed

   }

Meaning of "synchronized" in Java


    John: accnt.deposit(100)       
                                    Joe: accnt.deposit(100)    
    acquire the lock on accnt
   +--------------------------+     try to acquire the lock on accnt
   |                          |     since the lock is not available,
   | b = balance    (b = 150) |     go in the "waiting list" of accnt
   |   |                      |   
   |   |                      |        ... wait ...
   |   V                      |                       
   | b += 100       (b = 250) | 
   |   |                      |
   |   |                      |      
   |   V                      | 
   | balance = b              |        ... wait ...
   |                          |  
   +--------------------------+                            
    release the lock on accnt     
                                    acquire the lock on accnt
				   +--------------------------+
				   |                          |
				   | b = balance    (b = 250) |
				   |   |                      |
				   |   |                      |     
				   |   V                      |                
				   | b += 100       (b = 350) | 
				   |   |                      |
				   |   |                      | 
				   |   V                      | 
				   | balance = b              | 
				   |                          |   
                                   +--------------------------+
                                    release the lock on accnt                              


A few remarks about the locks

Communication among threads in Java

In Java threads can communicate essentially in two ways: The first method should be obvious, as the concept of memory and variable are well known from sequential programming already. Concurrency does not introduce anything new here except for the need of synchronization (for encapsulating critical sections).

The second method is specific to concurrent programming. We illustrate it with an example.

An example: buffered communication

Consider a producer-consumer system consisting of two threads, P and C, connected by a buffer B.
          
         +-------+
   P --->|   B   |---> C
         +-------+               

The constraints

  1. We assume that B can contain only one unit of information at the time (one-position buffer)
  2. All info produced by P must be consumed once and only once by C
  3. We might want to connect other producers or consumers to the same buffer (open system)

A first attempt

The following is a first attempt to solve the problem.

The interactions of the producer and the consumer with the buffer will be made possible by two synchronized methods put() and get()

   class Buffer {
       private Information content;

       public Buffer() { }
    
       public synchronized void put(Information i) {
           content = i;
       }
       public synchronized Information get() {
           return content;
       }
   }

   class Producer extends Thread {
       private Buffer buff;

       public Producer(Buffer b) { buff = b; }

       public void run() {
           while (...) {
               ...
               buff.put(info);
               ...
           }  
       }
   }

   class Consumer extends Thread {
       private Buffer buff;

       public Consumer(Buffer b) { buff = b; }

       public void run() {
           while (...) {
               ...
               x = buff.get();
               ...
           }  
       }
   }

Unfortunately, this code is not correct. In fact, it does not guarrantee the "once and only once" condition:

A second attempt

   class Buffer {
       private Information content;
       private boolean empty;
       ---------------------
       public Buffer() { empty = true; }
                         ------------
       public synchronized boolean is_empty() {
           return empty;           ----------
       }    
       public synchronized void put(Information i) {
           content = i; empty = false;
       }                -------------
       public synchronized Information get() {
           empty = true; return content; 
       }   ------------
   }

   class Producer extends Thread {
       ...
       public void run() {
               ...
               if (buff.is_empty())  buff.put(info); 
               --------------------
       }
   }

   class Consumer extends Thread {
       ...
       public void run() {
               ...
               if (!buff.is_empty())  x = buff.get(); 
               --------------------
       }
   }

Unfortunately, also this code is incorrect. In fact, the separation between the test and the put() (respectively, get()) operation might cause interference. In this particular case nothing bad can happen, but if we add another producer or consumer, then wrong results may be produced.

One way to solve this problem is by performing both the test and the put() (respectively, get()) operation inside the same synchronized method. This is the basic idea on which next solution is based.

A third attempt

For example the method put() would be defined as follows:
   class Buffer {
       ...
       public synchronized boolean put(Information i) {
           if (empty) {
               content = i; empty = false; return true; 
           } else
               return false; 
       }
       ...
   }
And, correspondingly, the producer would be defined as follows:

   class Producer extends Thread {
       ...
       public void run() {
               ...
               success = buff.put(info); 
               if (!success) { ... }
               ...
       }
   }
The code is now correct. However, what does the producer do if the put() operation does not succeed? There are two possibile answers:
  1. Something else
  2. Keeps trying performing the put() until it succeeds
    
       class Producer extends Thread {
           ...
           public void run() {
                   ...
                   success = buff.put(info); 
                   while (!success) { b = buff.put(info); }
                   ...
           }
       }
    
Note that the second case is unavoidable when the thread needs to perform that operation before proceeding.

With the second case there is a problem of efficiency called "busy waiting".

The problem of "busy waiting"

We say that a thread is in a situation of busy waiting if it is This situation creates a problem of efficiency because the thread keeps consuming the time of a processor just for performing a test. It would be better, of course, to suspend the thread until the condition is verified. This can be done in Java by using the methods wait() , notify() and notifyAll().

The correct solution

The following code is the correct solution for our problem. It avoids race conditions, respects the contraints of the problem, works also in presence of more Producers and Consumers, and avoids busy waiting.
   class Buffer {
       private Information content;
       private boolean empty;
       
       public Buffer() { empty = true; }
  
       public synchronized void put(Information i) 
         throws InterruptedException {
           while (!empty) wait();
           content = i;
           empty = false;
           notifyAll();
       }
       public synchronized int get() 
         throws InterruptedException {
           while (empty) wait();
           empty = true;
           notifyAll();
           return content;
       }
   }

   class Producer extends Thread {
       ...
       public void run() {
           while (...) {
               ...
               buff.put(info);
               ...
           }  
       }
   }

   class Consumer extends Thread {
       ...
       public void run() {
           while (...) {
               ...
               x = buff.get();
               ...
           }  
       }
   }

Comments on the methods wait() and notify()