Skip to main content
查利博客

Java Concurrency - Race Condition

A race condition in Java occurs when multiple threads access a shared resource without properly synchronization. Typically there are two types of race conditions: read-modify-write and check-then-act. They both involve a sequence of non-atomic operations and the solution is to serialize access to the mutable data by using synchronization techniques like synchronized keyword, locking, static initializers etc.

Read-Modify-Write Example #

An example of counting post views illustrates race condition:

public class ReadModifyWriteExample {

  public static void main(String[] args) throws InterruptedException {
    // shared object that will pass to threads
    Post post = new Post();
    Thread t1 = new Thread(new PostRunnable(post));
    Thread t2 = new Thread(new PostRunnable(post));

    t1.start();
    t2.start();

    // wait t1 and t2 complete their executions
    t1.join();
    t2.join();

    System.out.println("views: " + post.getViews());
  }
}

class PostRunnable implements Runnable {
  private Post post;

  public PostRunnable(Post post) {
    this.post = post;
  }

  @Override
  public void run() {
    for (int i = 0; i < 20000; ++i) {
      post.view();
    }
  }
}

class Post {
  private int views = 0;

  public void view() {
    views++;
  }

  public int getViews() {
    return views;
  }
}

Output:

23313

The expected total views should be 40,000 but instead we get 23,313.

Solution 1: Synchronized method

class Post {
  private int views = 0;

  public synchronized void view() {
    views++;
  }

  public int getViews() {
    return views;
  }
}

Solution 2: Synchronized block

class Post {
  private int views = 0;

  public void view() {
    synchronized (this) {
      views++;
    }
  }

  public int getViews() {
    return views;
  }
}

Solution 3: Object level lock

class Post {
  private int views = 0;
  private Object lock = new Object();

  public void view() {
    synchronized (lock) {
      views++;
    }
  }

  public int getViews() {
    return views;
  }
}

Check-Then-Act Example #

An example of lazy initlization illustrates race condition:

public class CheckThenActExample {

  public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
      var obj1 = HeavyObjectHolder.getInstance();
      System.out.println(Thread.currentThread().getName() + " : " + obj1.hashCode());
    });
    t1.setName("thread 1");
    Thread t2 = new Thread(() -> {
      var obj2 = HeavyObjectHolder.getInstance();
      System.out.println(Thread.currentThread().getName() + " : " + obj2.hashCode());
    });
    t2.setName("thread 2");

    t1.start();
    t2.start();
  }
}

class HeavyObjectHolder {
  private static HeavyObject heavyObject;

  public static HeavyObject getInstance() {
    if (heavyObject == null) {
      heavyObject = new HeavyObject();
    }
    return heavyObject;
  }
}

class HeavyObject {}

Output:

thread 2 : 549121544
thread 1 : 623578683

It is about one tenth of the time to get the same object (In Java, same hashcode does not necessarily equal to same object) as expected. This is because both thread 1 and thread 2 reads the same heavyObject as null at the same time.

Solution 1: Double-Checked Locking

class HeavyObjectHolder {
  private static volatile HeavyObject heavyObject;

  public static HeavyObject getInstance() {
    if (heavyObject == null) {
      synchronized (HeavyObjectHolder.class) {
        if (heavyObject == null) {
          heavyObject = new HeavyObject();
        }
      }
    }
    return heavyObject;
  }

  private HeavyObjectHolder() {}
}

Output:

thread 1 : 802383179
thread 2 : 802383179

Finally, we get the same hashcode! Check this post if you wonder why the volatile keyword is used in double-checked locking.

Solution 2: Static Initialization (Initialization-On-Demand)

class HeavyObjectHolder {
  private static class LazyHolder {
    private static final HeavyObject heavyObject = new HeavyObject();
  }

  public static HeavyObject getInstance() {
    return LazyHolder.heavyObject;
  }

  private HeavyObjectHolder() {}
}

The static class LazyHolder is only initialized when the static method getInstance is invoked. For more details, see this page.

Solution 3: Use Early Initialization Insetad

class HeavyObjectHolder {
  private static final HeavyObject heavyObject = new HeavyObject();

  public static HeavyObject getInstance() {
    return heavyObject;
  }

  private HeavyObjectHolder() {}
}

Solution 4: Enum

enum HeavyObjectHolder {
  HeavyObject;
}

Conclusion #

To conclude, you should always make sure you have properly synchronized mutable data to avoid race condition. Or just declare all your variables inside the method block as threads have their own stacks such that the references (variables inside method) are not exposed to the world!