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!