Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Learn › Java › Threads and concurrency

Race condition

Why does count++ lose increments?

Several threads increment the same variable at the same time. count++ consists of three steps (read, add, and write), so when the steps of different threads are interleaved, increments are lost. A situation like this, where the result depends on the order in which the threads run, is called a race condition.

Checked against real execution · The same code was run on JDK 21

Concepts in this lab

Race condition
This is a situation in which two or more threads read and write the same data at the same time, and the result depends on the order of execution. count++ consists of three steps: read the value, add 1, and write it back. If the steps of two threads are interleaved, one of the increments is lost. It typically stays hidden in tests and appears under load.
Critical section and mutual exclusion
A critical section is a section of code that causes problems if several threads run it at the same time. Mutual exclusion guarantees that only one thread at a time runs that section, and it is implemented with a lock. While one thread holds the lock, the other threads wait.
synchronized
This Java keyword marks a critical section. Only one thread at a time can run a method or block marked with it, and a thread that wants to run it must first acquire the monitor lock of the object involved. It is simple, but a thread that waits for the lock is stopped, so keep the scope only as large as needed.
volatile
This keyword, when placed on a variable, guarantees that a value written by one thread is always read as the latest value by other threads (a visibility guarantee). However, it does not combine a read-modify-write compound operation into one, so it does not make an operation such as count++ safe. It suits a status flag that one thread writes and several threads read.
Visibility
This refers to whether a value changed by one thread can be seen by other threads. Without synchronization, CPU caches and compiler optimizations can let another thread keep seeing the value from before the change. synchronized, volatile, and the tools in java.util.concurrent guarantee it.
Atomicity and CAS (compare-and-swap)
Atomicity is the property that an operation made of several steps runs as one, with no gap for anything to cut in. CAS (compare-and-swap) performs "change to the new value only if the current value equals the value I read" as a single atomic operation, which makes safe updates possible without locks. AtomicInteger and similar classes use it.
Thread safety
This is the property of working correctly even when several threads use something at the same time. Code is thread-safe if there is no shared state, if the state is immutable, or if access is properly synchronized. A Spring singleton bean is used by many request threads together, so it must be made thread-safe (usually by keeping no state).

Try these

Running with the default settings

  1. Thread A: It read count. The value read is 0.
  2. Thread B: It read count. The value read is 0.
  3. Thread A: It added 1 to 0, the value it read, and got 1. It has not written the result to count yet.
  4. Thread B: It added 1 to 0, the value it read, and got 1. It has not written the result to count yet.
  5. Thread A: It wrote 1 to count.
  6. Thread B: It wrote 1 to count. But since this thread read the value, other threads had raised count to 1. The thread overwrote that value with 1. Increments lost by this write: 1.
  7. Thread A: It read count. The value read is 1.
  8. Thread B: It read count. The value read is 1.
  9. Thread A: It added 1 to 1, the value it read, and got 2. It has not written the result to count yet.
  10. Thread B: It added 1 to 1, the value it read, and got 2. It has not written the result to count yet.
  11. Thread A: It wrote 2 to count.
  12. Thread B: It wrote 2 to count. But since this thread read the value, other threads had raised count to 2. The thread overwrote that value with 2. Increments lost by this write: 1.
  13. Thread A: It read count. The value read is 2.
  14. Thread B: It read count. The value read is 2.
  15. Thread A: It added 1 to 2, the value it read, and got 3. It has not written the result to count yet.
  16. Thread B: It added 1 to 2, the value it read, and got 3. It has not written the result to count yet.
  17. Thread A: It wrote 3 to count.
  18. Thread B: It wrote 3 to count. But since this thread read the value, other threads had raised count to 3. The thread overwrote that value with 3. Increments lost by this write: 1.

Result of this run

Code

static int count = 0;

Runnable task = () -> {
    for (int i = 0; i < 3; i++) {
        count++;                    // read -> add -> write
    }
};

// 2 threads run the same task at the same time
for (int t = 0; t < 2; t++) {
    new Thread(task).start();
}

Assumptions of this model

The model is simplified so that only one step of one thread runs at a time. On a real machine with multiple cores, several threads truly run at the same time. The model also assumes that a read always sees the latest value, but with a real ordinary variable, a value written by another thread may not be visible (a visibility problem). When real code in which 2 threads each increment 1 million times was run with JDK 21 on this PC, count++ without synchronization gave a different result every time, from 1,017,250 to 1,892,129, the result still fell short with volatile, and synchronized and AtomicInteger always gave 2,000,000. AtomicInteger is drawn in the order read → CAS → retry on failure, but depending on the CPU, a real JVM sometimes completes this increment with a single instruction, so retries may not be noticeable.

Previous lab: Creating threads and thread states · Next lab: Deadlock

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these