Race Conditions, Synchronized and Deadlock

cpu-head

When we are working with multithreading implementations, we have to consider the behavior of our code when multiple threads try to access it. We need to make sure our threads do not fight and work in peace and harmony. To begin with, let’s start with race condition.

Race Conditions:

These are situations where non atomic operations give us unexpected result. Refer the code below.

public class RaceCondition {
  public static void main(String[] args) throws InterruptedException {

    SharedClass sharedObject = new SharedClass();

    Thread addThread = new Thread(() -> {
      int i = 0;
      while (i < 10000) {
        sharedObject.addOne();
        i++;
      }
    });

    Thread minusThread = new Thread(() -> {
      int i = 0;
      while (i < 10000) {
        sharedObject.minusOne();
        i++;
      }
    });

    addThread.start();
    minusThread.start();
    addThread.join();
    minusThread.join();
    System.out.println("Final Result === " + sharedObject.getResult());
  }
}

class SharedClass {
  int x = 0;

  public void addOne() {
    x++;
  }

  public void minusOne() {
    x--;
  }

  public int getResult() {
    return x;
  }
}

If we run the above code, we observe the output is not as expected. Since we are adding and subtracting 10000 times, we should expect the initialized value of the variable as the final result i.e. 0, but we get a different number in every run.

In the code above, SharedClass methods addOne() and minusOne() are doing a very simple task of adding and subtracting one from variable “x” respectively. But even these operations are also achieved by multiple different instructions by CPU and are not atomic – Adding one to the variable (x+1) and then assigning the final value to variable x.

To resolve the above issue, we will use synchronized keyword.

Keyword “synchronized”:

We can use synchronized keyword on method level or section level.

On updating our SharedClass as below, we can see the code is behaving as expected and giving us consistent final result as “0”.

class SharedClass {
  int x = 0;

  public synchronized void addOne() {
    x++;
  }

  public synchronized void minusOne() {
    x--;
  }

  public int getResult() {
    return x;
  }
}
  • On method level, synchronized keyword will ensure only one thread can access one of the synchronized methods and block the execution of other thread. If you have multiple methods marked with synchronized keyword. Only one thread can access them at a time.
  • On section level, we can allow multiple threads to access different sections at same time by creating different objects for locking instead of just one (object1).

Below is an example of section level use of synchronized keyword.

class SharedClass {
  int x = 0;
  Object object1 = new Object();
  Object object2 = new Object();

  public void addOne() {
    synchronized (object1) {
      x++;
    }
  }

  public void minusOne() {
    synchronized (object2) {
      x--;
    }
  }

  public int getResult() {
    return x;
  }
}
Deadlock:

In deadlock situation, there are at least two threads which are trying to complete their task but need an object which is currently locked by other thread. For example: Thread1 have lock on objectA and need lock on objectB to complete its task whereas Thread2 have lock on objectB and need lock on objectA to complete its task. In such situation both Threads are in Deadlock.

Refer below code sample which can lead to deadlock when two different threads access methods methodAtoB and methodBtoA.

class SharedDeadlockClass {
  Object objectA = new Object();
  Object objectB = new Object();

  public void methodAtoB() {
    synchronized (objectA) {
      System.out.println("Having lock on objectA. Going to acquire lock on objectB.");
      synchronized (objectB) {
        System.out.println("Having lock on objectB.");
      }
    }
  }

  public void methodBtoA() {
    synchronized (objectB) {
      System.out.println("Having lock on objectB. Going to acquire lock on objectA.");
      synchronized (objectA) {
        System.out.println("Having lock on objectA.");
      }
    }
  }
}

To resolve deadlock issue, we need to correct the program structure to avoid creating deadlock at first place. Refer below corrected structure. We basically ensure the order of locking objects is same.

class SharedDeadlockClass {
  Object objectA = new Object();
  Object objectB = new Object();

  public void methodAtoB() {
    synchronized (objectA) {
      System.out.println("Having lock on objectA. Going to acquire lock on objectB.");
      synchronized (objectB) {
        System.out.println("Having lock on objectB.");
      }
    }
  }

  public void methodBtoA() {
    synchronized (objectA) {
      System.out.println("Having lock on objectA. Going to acquire lock on objectB.");
      synchronized (objectB) {
        System.out.println("Having lock on objectB.");
      }
    }
  }
}

Leave a Comment

Your email address will not be published. Required fields are marked *