Atomicity: Why One Line Is Not One Step

An operation is atomic when it happens, from the point of view of every other thread, as a single indivisible step — either it has not happened yet, or it is completely done, with no state in between ever visible to anyone else. Almost nothing in Java is atomic by default, including things that look like one instruction.

count++ is three steps, not one

public class NotAtomic {
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 100_000; i++) {
                count++;
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Expected 200000, got " + count);
    }
}

count++ compiles to roughly three separate bytecode operations: read the current value of count, add one to it, and write the new value back. Two threads can each read the same value, each compute the same "plus one" result, and each write back the same number, silently losing one of the two increments. Nothing about count++ looks like three steps in the source code, which is exactly why this class of bug catches even experienced developers off guard.

The same shape hides in higher-level code

The read-modify-write pattern is not limited to arithmetic. Any "look something up, then decide what to do based on it" sequence has the same structure:

import java.util.HashMap;
import java.util.Map;

public class NotAtomicMap {
    static final Map<String, Integer> visits = new HashMap<>();

    static void recordVisit(String page) {
        Integer current = visits.get(page);       // read
        int updated = (current == null) ? 1 : current + 1;
        visits.put(page, updated);                 // write
    }
}

recordVisit has the same read-then-write gap as count++, just spread across two method calls instead of one operator. Two threads calling recordVisit("home") at nearly the same moment can both read the same current value and both write back the same updated value, losing a visit — in addition to the fact that plain HashMap is not even safe to mutate concurrently at all, which is a separate problem covered in a later phase.

Why "it's just one line" is the wrong intuition

Java source syntax has no concept of atomicity. A single statement can compile down to any number of bytecode instructions, and the JVM is free to let another thread run in between any two of them unless something explicitly prevents it. The size or simplicity of a line of source code tells you nothing about whether it is safe to run from multiple threads at once; the only thing that matters is whether the underlying operation is documented and guaranteed to be atomic. AtomicInteger.incrementAndGet(), for instance, guarantees atomicity for exactly this increment case — the tools for making operations atomic are covered in the next phase of this series. What matters here is recognizing the shape of the problem: any read-modify-write sequence on shared state, however it is written, is not safe until proven otherwise.

Share