Core Java

Java Concurrency Tutorial – Atomicity and race conditions

Atomicity is one of the key concepts in multi-threaded programs. We say a set of actions is atomic if they all execute as a single operation, in an indivisible manner. Taking for granted that a set of actions in a multi-threaded program will be executed serially may lead to incorrect results. The reason is due to thread interference, which means that if two threads execute several steps on the same data, they may overlap.

The following Interleaving example shows two threads executing several actions (prints in a loop) and how they are overlapped:

 
 
 

public class Interleaving {
    
    public void show() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " - Number: " + i);
        }
    }
    
    public static void main(String[] args) {
        final Interleaving main = new Interleaving();
        
        Runnable runner = new Runnable() {
            @Override
            public void run() {
                main.show();
            }
        };
        
        new Thread(runner, "Thread 1").start();
        new Thread(runner, "Thread 2").start();
    }
}

When executed, it will produce unpredictable results. As an example:

Thread 2 - Number: 0
Thread 2 - Number: 1
Thread 2 - Number: 2
Thread 1 - Number: 0
Thread 1 - Number: 1
Thread 1 - Number: 2
Thread 1 - Number: 3
Thread 1 - Number: 4
Thread 2 - Number: 3
Thread 2 - Number: 4

In this case, nothing wrong happens since they are just printing numbers. However, when you need to share the state of an object (its data) without synchronization, this leads to the presence of race conditions.

Race condition

Your code will have a race condition if there’s a possibility to produce incorrect results due to thread interleaving. This section describes two types of race conditions:

  1. Check-then-act
  2. Read-modify-write

To remove race conditions and enforce thread safety, we must make these actions atomic by using synchronization. Examples in the following sections will show what the effects of these race conditions are.

Check-then-act race condition

This race condition appears when you have a shared field and expect to serially execute the following steps:

  1. Get a value from a field.
  2. Do something based on the result of the previous check.

The problem here is that when the first thread is going to act after the previous check, another thread may have interleaved and changed the value of the field. Now, the first thread will act based on a value that is no longer valid. This is easier seen with an example.

UnsafeCheckThenAct is expected to change the field number once. Following calls to changeNumber method, should result in the execution of the else condition:

public class UnsafeCheckThenAct {
    private int number;
    
    public void changeNumber() {
        if (number == 0) {
            System.out.println(Thread.currentThread().getName() + " | Changed");
            number = -1;
        }
        else {
            System.out.println(Thread.currentThread().getName() + " | Not changed");
        }
    }
    
    public static void main(String[] args) {
        final UnsafeCheckThenAct checkAct = new UnsafeCheckThenAct();
        
        for (int i = 0; i < 50; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    checkAct.changeNumber();
                }
            }, "T" + i).start();
        }
    }
}

But since this code is not synchronized, it may (there's no guarantee) result in several modifications of the field:

T13 | Changed
T17 | Changed
T35 | Not changed
T10 | Changed
T48 | Not changed
T14 | Changed
T60 | Not changed
T6 | Changed
T5 | Changed
T63 | Not changed
T18 | Not changed

Another example of this race condition is lazy initialization.

A simple way to correct this is to use synchronization.

SafeCheckThenAct is thread-safe because it has removed the race condition by synchronizing all accesses to the shared field.

public class SafeCheckThenAct {
    private int number;
    
    public synchronized void changeNumber() {
        if (number == 0) {
            System.out.println(Thread.currentThread().getName() + " | Changed");
            number = -1;
        }
        else {
            System.out.println(Thread.currentThread().getName() + " | Not changed");
        }
    }
    
    public static void main(String[] args) {
        final SafeCheckThenAct checkAct = new SafeCheckThenAct();
        
        for (int i = 0; i < 50; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    checkAct.changeNumber();
                }
            }, "T" + i).start();
        }
    }
}

Now, executing this code will always produce the same expected result; only a single thread will change the field:

T0 | Changed
T54 | Not changed
T53 | Not changed
T62 | Not changed
T52 | Not changed
T51 | Not changed
...

In some cases, there will be other mechanisms which perform better than synchronizing the whole method but I won’t discuss them in this post.

Read-modify-write race condition

Here we have another type of race condition which appears when executing the following set of actions:

  1. Fetch a value from a field.
  2. Modify the value.
  3. Store the new value to the field.

In this case, there’s another dangerous possibility which consists in the loss of some updates to the field. One possible outcome is:

Field’s value is 1.
Thread 1 gets the value from the field (1).
Thread 1 modifies the value (5).
Thread 2 reads the value from the field (1).
Thread 2 modifies the value (7).
Thread 1 stores the value to the field (5).
Thread 2 stores the value to the field (7).

As you can see, update with the value 5 has been lost.

Let’s see a code sample. UnsafeReadModifyWrite shares a numeric field which is incremented each time:

public class UnsafeReadModifyWrite {
    private int number;
    
    public void incrementNumber() {
        number++;
    }
    
    public int getNumber() {
        return this.number;
    }
    
    public static void main(String[] args) throws InterruptedException {
        final UnsafeReadModifyWrite rmw = new UnsafeReadModifyWrite();
        
        for (int i = 0; i < 1_000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    rmw.incrementNumber();
                }
            }, "T" + i).start();
        }
        
        Thread.sleep(6000);
        System.out.println("Final number (should be 1_000): " + rmw.getNumber());
    }
}

Can you spot the compound action which causes the race condition?

I’m sure you did, but for completeness, I will explain it anyway. The problem is in the increment (number++). This may appear to be a single action but in fact, it is a sequence of three actions (get-increment-write).

When executing this code, we may see that we have lost some updates:

2014-08-08 09:59:18,859|UnsafeReadModifyWrite|Final number (should be 10_000): 9996

Depending on your computer it will be very difficult to reproduce this update loss, since there’s no guarantee on how threads will interleave. If you can’t reproduce the above example, try UnsafeReadModifyWriteWithLatch, which uses a CountDownLatch to synchronize thread’s start, and repeats the test a hundred times. You should probably see some invalid values among all the results:

Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 997
Final number (should be 1_000): 999
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000

This example can be solved by making all three actions atomic.

SafeReadModifyWriteSynchronized uses synchronization in all accesses to the shared field:

public class SafeReadModifyWriteSynchronized {
    private int number;
    
    public synchronized void incrementNumber() {
        number++;
    }
    
    public synchronized int getNumber() {
        return this.number;
    }
    
    public static void main(String[] args) throws InterruptedException {
        final SafeReadModifyWriteSynchronized rmw = new SafeReadModifyWriteSynchronized();
        
        for (int i = 0; i < 1_000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    rmw.incrementNumber();
                }
            }, "T" + i).start();
        }
        
        Thread.sleep(4000);
        System.out.println("Final number (should be 1_000): " + rmw.getNumber());
    }
}

Let’s see another example to remove this race condition. In this specific case, and since the field number is independent to other variables, we can make use of atomic variables.

SafeReadModifyWriteAtomic uses atomic variables to store the value of the field:

public class SafeReadModifyWriteAtomic {
    private final AtomicInteger number = new AtomicInteger();
    
    public void incrementNumber() {
        number.getAndIncrement();
    }
    
    public int getNumber() {
        return this.number.get();
    }
    
    public static void main(String[] args) throws InterruptedException {
        final SafeReadModifyWriteAtomic rmw = new SafeReadModifyWriteAtomic();
        
        for (int i = 0; i < 1_000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    rmw.incrementNumber();
                }
            }, "T" + i).start();
        }
        
        Thread.sleep(4000);
        System.out.println("Final number (should be 1_000): " + rmw.getNumber());
    }
}

Following posts will further explain mechanisms like locking or atomic variables.

Conclusion

This post explained some of the risks implied when executing compound actions in non-synchronized multi-threaded programs. To enforce atomicity and prevent thread interleaving, one must use some type of synchronization.

  • You can take a look at the source code at github.

Xavier Padro

Xavier is a software developer working in a consulting firm based in Barcelona. He is specialized in web application development with experience in both frontend and backend. He is interested in everything related to Java and the Spring framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button