Synchronizing Threads in Java

To get work done, threads often need to communicate with each other. Communication can be done by message passing or through shared data structures in the main threads heap. This can lead to synchronization problems.

Example: A joint bank account

class BankAccount

class BankAccount {
   private double balance;
   public BankAccount(double bal) { balance = bal; }
   public BankAccount() { this(0); }
   public double getBalance() { return balance; }
   public void deposit(double amt) { ... }
   public void withdraw(double amt) { ... }
}

deposit()

   public void deposit(double amt) {
      double temp = balance;
      temp = temp + amt;
      try {
         Thread.sleep(300); // simulate production time
      } catch (InterruptedException ie) {
         System.err.println(ie.getMessage());
      }
      System.out.println("after deposit balance = $" + temp);
      balance = temp;
   }

withdraw()

   public void withdraw(double amt) {
      if (balance < amt) {
         System.out.println("Insufficient funds!");
         return;
      }
      double temp = balance;
      temp = temp - amt;
      try {
         Thread.sleep(200); // simulate consumption time
      } catch (InterruptedException ie) {
         System.err.println(ie.getMessage());
      }
      System.out.println("after withdrawl balance = $" + temp);
      balance = temp;
   }

class Producer

A typical producer deposits $10 x 5 = $50:

class Producer extends Thread {
   private BankAccount account;
   public Producer(BankAccount acct) { account = acct; }
   public void run() {
      for(int i = 0; i < 5; i++) {
         account.deposit(10);
      }
   }
}

class Consumer

A typical consumer withdraws $10 x 5 = -$50:

class Consumer extends Thread {
   private BankAccount account;
   public Consumer(BankAccount acct) { account = acct; }
   public void run() {
      for(int i = 0; i < 5; i++) {
         account.withdraw(10);
      }
   }
}

class Bank

The master thread is the bank. It creates an account with an initial balance of $100. It creates 4 account holders, 2 producers and 2 consumers:

public class Bank {
   public static void main(String[] args) {
      BankAccount account = new BankAccount(100);
      int slaveCount = 4;
      Thread[] slaves = new Thread[slaveCount];
      for(int i = 0; i < slaveCount; i++) {
         if (i % 2 == 0) {
            slaves[i] = new Producer(account);
         } else {
            slaves[i] = new Consumer(account);
         }
      }
      for(int i = 0; i < slaveCount; i++) {
         slaves[i].start();
      }
      for(int i = 0; i < slaveCount; i++) {
         try {
            slaves[i].join();
         } catch(InterruptedException ie) {
               System.err.println(ie.getMessage());
         } finally {
            System.out.println("slave "+ i + " has died");
         }
      }
      System.out.print("Closing balance = ");
      System.out.println("$" + account.getBalance());
   }
}

Program output

after withdrawl balance = $90.0
after deposit balance = $110.0
after deposit balance = $110.0
after withdrawl balance = $80.0
after withdrawl balance = $80.0
after deposit balance = $120.0
after deposit balance = $120.0
after withdrawl balance = $70.0
after withdrawl balance = $70.0
after withdrawl balance = $60.0
after withdrawl balance = $60.0
after deposit balance = $130.0
after deposit balance = $130.0
after withdrawl balance = $50.0
after withdrawl balance = $50.0
after deposit balance = $140.0
after deposit balance = $140.0
after deposit balance = $150.0
slave 0 has died
after deposit balance = $150.0
slave 1 has died
slave 2 has died
slave 3 has died
Closing balance = $150.0

The problem

Notice that the "insufficient funds" message never appeared. That means both consumers successfully withdrew $10 five times each for a total of -$100. Similarly, both producers successfully deposited $10 five times each for a total of $100. The initial balance was $100, so the closing balance should have been:

balance = $100 + $100 - $100 = $100

Somehow, the bank lost $50.

It seems pretty clear what went wrong. Look at the first two lines of output:

after withdrawl balance = $90.0
after deposit balance = $110.0

Apparently, when the first producer was in the middle of making a deposit, he was interrupted by a consumer who quickly withdrew $10, leaving the balance at $90. Unfortunately, the last time the producer checked, the balance was $100, so after adding an additional $10, the producer now believes the balance is $110. In other words, the bank lost track of the $10 withdrawn by the consumer.

Indivisibility

Apparently the problem is the leisurely pace of the deposit and withdraw methods, which afford plenty of time to be interrupted. Perhaps if we shortened these methods to:

void deposit(double amt) { balance += amt; }
void withdraw(double amt) { balance -= amt; }

This apparently works until we increase the number of deposits and withdrawals from 5 times to 30,000 times. Eventually, a discrepancy in the balance arises.

The problem is that although speedy, the body of these methods still translates into multiple assembly language instructions. For example,

balance += amt;

translates to:

move balance, register1
move amount, register2
add register1, register2
move register2, balance

Although an individual assembly language instruction executes without interruption, it's still possible-- although not likely-- that the consumer may interrupt the producer somewhere between the first and last instruction.

Solution 1.0: Locks

Every object in Java has a lock that can be held (i.e., locked) by at most one thread at a time, and a queue of suspended threads waiting to hold this lock:

class Object {
   private Thread holder = null; // holder of this object's lock
   private Queue synchQueue; // threads waiting to be the holder
   // etc.
}

The current thread attempts to hold an object's lock by using a synchronization block:

synchronized(obj) { ... }

In this case the thread enters a special "attempting to lock obj" state. If the holder of the object's lock is not null, then the current thread is suspended and placed in the set of threads waiting to hold the object's lock.

If the holder of the lock is null, then the current thread becomes the holder:

obj.holder = Thread.currentThread();

The thread returns to its ready state and eventually back to its running state, where it proceeds to execute the subsequent block. Upon exiting the block, the thread selects a suspended thread from the synchQueue, makes it the holder of the thread, and resumes its execution:

obj.holder = synchQueue.first();
obj.holder.resume();

We may view waiting in a synchronization queue of some object-- mutex, say-- as a new thread state-- "attempting to lock mutex." Let's add this to our state diagram:

In our banking example, we can make the producer and consumer synchronize on the account's associated lock:

class Producer extends Thread {
   private BankAccount account;
   public Producer(BankAccount acct) { account = acct; }
   public void run() {
      for(int i = 0; i < 5; i++) {
         synchronized(account) { account.deposit(10); }
      }
   }
}

 

class Consumer extends Thread {
   private BankAccount account;
   public Consumer(BankAccount acct) { account = acct; }
   public void run() {
      for(int i = 0; i < 5; i++) {
         synchronized(account) { account.withdraw(10); }
      }
   }
}

Program Output

after withdrawl balance = $100.0
after deposit balance = $110.0
after withdrawl balance = $100.0
after deposit balance = $110.0
after withdrawl balance = $100.0
after deposit balance = $110.0
after withdrawl balance = $100.0
after deposit balance = $110.0
after withdrawl balance = $100.0
after deposit balance = $110.0
after withdrawl balance = $100.0
after deposit balance = $110.0
after withdrawl balance = $100.0
after deposit balance = $110.0
after withdrawl balance = $100.0
after deposit balance = $110.0
slave 0 has died
after withdrawl balance = $100.0
slave 1 has died
after deposit balance = $110.0
slave 2 has died
after withdrawl balance = $100.0
slave 3 has died
Closing balance = $100.0

Solution 1.1: Locks with notification conditions

Let's alter the problem. Assume the initial balance in the account is $0. Instead of two consumers and two producers, suppose there are three consumers and just one producer. Assume the producer deposits $10 15 times for a total of $150, while each consumer attempts to withdraw $10 five times for a total of $10 x 5 x 3 = -$150. Here's the output produced:

after deposit balance = $10.0
after withdrawl balance = $0.0
Insufficient funds!
Insufficient funds!
after deposit balance = $10.0
after withdrawl balance = $0.0
Insufficient funds!
Insufficient funds!
after deposit balance = $10.0
after withdrawl balance = $0.0
Insufficient funds!
Insufficient funds!
after deposit balance = $10.0
after withdrawl balance = $0.0
Insufficient funds!
Insufficient funds!
after deposit balance = $10.0
after withdrawl balance = $0.0
Insufficient funds!
Insufficient funds!
after deposit balance = $10.0
after deposit balance = $20.0
after deposit balance = $30.0
after deposit balance = $40.0
after deposit balance = $50.0
after deposit balance = $60.0
after deposit balance = $70.0
after deposit balance = $80.0
after deposit balance = $90.0
after deposit balance = $100.0
slave 0 has died
slave 1 has died
slave 2 has died
slave 3 has died
Closing balance = $100.0

Notice all of the "Insufficient funds!" messages. This is the sort of thing that makes economists cry. The supply was sufficient to meet the demand, but the demand went unmet because of a mere timing problem.

In addition to a lock and a synchronization queue, every Java object also maintains a queue of suspended threads awaiting notification.

class Object {
   private Thread holder = null; // holder of this object's lock
   private Queue synchQueue; // threads waiting to be the holder
   private Queue notifyQueue; // threads waiting for notification
   // etc.
}

The holder of the object's lock releases the lock and joins this queue by calling one of the object's wait methods:

obj.wait();       // wait for notification
obj.wait(ms);     // wait for notification or timeout
obj.wait(ms, ns); // wait for notification or timeout

When the current holder of the object's lock calls one of the object's notification methods:

obj.notify();     // notify some thread on obj.notifyQueue
obj.notifyAll(); // notify all threads on obj.notifyQueue

The notified threads are moved to the synchQueue, where they resume competing for the object's lock. Note: Java does not actually specify which thread in the notifyQueue will be notified.

We can regard waiting in the notification queue of some object-- mutex, say-- as a new thread state-- "waiting for notification from mutex." Let's add this final state to our state diagram:

We can use this to solve our timing problem by having consumers wait for sufficient funds:

class Consumer extends Thread {
   private BankAccount account;
   public Consumer(BankAccount acct) { account = acct; }
   public void run() {
      for(int i = 0; i < 5; i++) {
         synchronized(account) {
            while(account.getBalance() < 10) {
               try {
                  account.wait();
               } catch (InterruptedException ie) {
                  System.err.println(ie.getMessage());
               }
            }
            account.withdraw(10);
         }
      }
   }
}

The producer notifies one of the consumers after each deposit:

class Producer extends Thread {
   private BankAccount account;
   public Producer(BankAccount acct) { account = acct; }
   public void run() {
      for(int i = 0; i < 15; i++) {
         synchronized(account) {
            account.deposit(10);
            account.notify();
         }
      }
   }
}

Here's the output produced:

after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
after withdrawl balance = $0.0
after deposit balance = $10.0
slave 0 has died
slave 1 has died
slave 2 has died
after withdrawl balance = $0.0
slave 3 has died
Closing balance = $0.0

Solution 2.0: Monitors

The problem with using locks is that if just one thread forgets to wait for the lock associated with an object, then the entire program gets out of synch and an error results. A better solution is to use a monitor. A monitor is a class with a private state. All public methods that access the monitor's state require the current thread to hold the monitor's lock:

class Monitor {
   private int state = 0;
   public void update(int newState) {
      synchronized(this) { state = newState; }
   }
}

Java provides an alternate syntax:

class Monitor {
   private int state = 0;
   public synchronized void update(int newState) {
      state = newState;
   }
}

Let's modify the BankAccount class:

class BankAccount {
   private double balance;
   public BankAccount(double bal) { balance = bal; }
   public BankAccount() { this(0); }
   public synchronized double getBalance() { return balance; }
   public synchronized void deposit(double amt) {
      double temp = balance;
      temp = temp + amt;
      try {
         Thread.sleep(300); // simulate production time
      } catch (InterruptedException ie) {
         System.err.println(ie.getMessage());
      }
      System.out.println("after deposit balance = $" + temp);
      balance = temp;
      notify();
   }
   public synchronized void withdraw(double amt) {
      while (balance < amt) {
         try {
            wait(); // wait for funds
         } catch (InterruptedException ie) {
            System.err.println(ie.getMessage());
         }
      }
      double temp = balance;
      temp = temp - amt;
      try {
         Thread.sleep(200); // simulate consumption time
      } catch (InterruptedException ie) {
         System.err.println(ie.getMessage());
      }
      System.out.println("after withdrawl balance = $" + temp);
      balance = temp;
   }
}

Now our threads can revert back to their original simplified form without worrying about synchronization issues:

class Consumer extends Thread {
   private BankAccount account;
   public Consumer(BankAccount acct) {
      account = acct;
   }
   public void run() {
      for(int i = 0; i < 5; i++) {
         account.withdraw(10);
      }
   }
}

class Producer extends Thread {
   private BankAccount account;
   public Producer(BankAccount acct) {
      account = acct;
   }
   public void run() {
      for(int i = 0; i < 15; i++) {
         account.deposit(10);
      }
   }
}