Java Concurrency - Synchronnized

Multithread applications are more efficient but if shared resources are not protected, race condition may occur and cause unexpected behavior. We need to properly synchronize

Race Condition

A race condition is the condition of an electronics, software, or other system where the system’s substantive behavior is dependent on the sequence or timing of other uncontrollable events. It becomes a bug when one or more of the possible behaviors is undesirable.

A simple Counter demo class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Counter {
private long count;

public long getCount() {
return count;
}

public void setCount(long count) {
this.count = count;
}

public void increment() {
count++;
}
}

If more than one thread calls setCount or increment method, we will have a race condition.

Note that increment() method has only one statement count++. count++ is not atomic. When compiled to bytecode, it becomes multiple operations. First it reads count variable, then add 1. Finally set the result to the original variable.

Sequence of events that cause race condition

count in memory thread1 thread2
0 read count(0) into register
0 read count(0) into register
0 count incremnt by 1
1 write count to memory. count is now 1
1 count increment by 1
1 write cocunt to memory. count is now 1

Here the increment method is called by both threads and the result should be 2. However, the result is 1 because of the race condition.

We can use Junit test that demonstrate the effects of race condition. The test below uses a thread pool to run increment() method 1000 times. The count doesn’t add up to 1000 because of race condition.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void raceConditionDemo() throws InterruptedException {
Counter counter = new Counter();
ExecutorService executor = Executors.newFixedThreadPool(20);
List<Callable<String>> myCallables = Collections.nCopies(1000, () -> {
counter.increment();
return null;
});
executor.invokeAll(myCallables);
TimeUnit.SECONDS.sleep(1);
executor.shutdown();
assertEquals(1000, counter.getCount()); // count should be 1000 because we call increment() 1000 times.
}

The actual count varies for each invocation:

1
2
3
org.opentest4j.AssertionFailedError: 
Expected :1000
Actual :989

How to solve Race Condition

To solve race condition issue, we often need to make sure critical section is executed by one thread only at any time. In Java, we can use synchronized keyword on a method or code block to synchronize resource access. When one thread is execute the code block with synchronized keyword, other threads need to wait until the thread finish the code block and release the lock.

Synchronized method

Add synchronized keyword to make a method synchronized.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Counter {
private long count;

public long getCount() {
return count;
}

public synchronized void setCount(long count) {
this.count = count;
}

public synchronized void increment() {
count++;
}
}

To make a method synchronized has two effects:

First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object.
Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads.

Intrinsic Locks

Every object has a lock associated with it. That is the Intrinsic Lock of the object.

If a thread needs to access an object’s fields, it first needs to own the intrinsic lock of the object. Other threads that do own an object’s intrinsic lock can’t access the object’s fields until the owning thread release the intrinsic lock.

When a thread invokes a synchronized method, it automatically acquires the intrinsic lock for that method’s object and releases it when the method returns.

Synchronized statement

You can use synchronized statement instead of synchronized method to coordinate resource access.

To use synchronized statement, you need to specify the object that provides the intrinsic lock.

use of synchronized statement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Counter {
private long count;

public long getCount() {
synchronized (this) {
return count;
}
}

public void setCount(long count) {
synchronized (this) {
this.count = count;
}
}

public void increment() {
synchronized (this) {
count++;
}
}
}

synchronized statement provides greater flexibility than synchronized method. You use it when you need fine-grain synchronization.

Here is a more practical use of synchronized statement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ComputerCount {
private long pcCount;
private long macCount;

private Object pcCountLock = new Object();
private Object macCountLock = new Object();

public long getPcCount() {
synchronized (pcCountLock) {
return pcCount;
}
}

public long getMacCount() {
synchronized (macCountLock) {
return macCount;
}
}

public void pcIncr() {
synchronized (pcCountLock) {
pcCount++;
}
}

public void macIncr() {
synchronized (macCountLock) {
synchronized ( macCountLock ) {
macCount++;
}
}
}
}

Reference