What Is a Race Condition?

A race condition is a flaw that occurs when the correctness of a program depends on the relative timing or interleaving of concurrent operations on shared data. When two or more threads (or processes, or machines) access the same state at the same time and at least one of them changes it, the final result can differ from run to run — and is often wrong.

The name captures the problem exactly: the operations are racing, and whoever wins the race determines the outcome. Because that ordering is non-deterministic, race conditions produce bugs that appear intermittently, pass in testing, and surface under production load.

A Simple Example

The classic race condition is a lost update on a shared counter. The code below looks harmless:

public class Counter {
    private int count = 0;

    public void increment() {
        count++;   // looks atomic — it isn't
    }

    public int get() {
        return count;
    }
}

The problem is that count++ is not a single operation. The JVM executes it as three steps: read the current value, add one, write the result back. When a single thread runs it, the steps happen in order. When two threads run it at the same time, they can interleave:

StepThread AThread BStored value
1read count → 00
2read count → 00
3add 1 (local = 1)0
4add 1 (local = 1)0
5write 11
6write 11

Both threads incremented, but the counter only advanced by one. One update was silently lost. Run increment() from a few hundred threads and the final value will reliably fall short of the expected total — by a different amount each time.

Why Race Conditions Happen

Every race condition needs three ingredients:

  • Shared mutable state — data that more than one thread can both read and write.
  • Concurrent access — that state is touched by multiple threads at overlapping times.
  • A non-atomic operation — the work is made of several steps that can be interrupted partway through.

Remove any one of these and the race disappears. That is why immutability, thread confinement, and atomic operations are the foundations of safe concurrent code: each one eliminates a required ingredient.

Race Condition vs. Deadlock vs. Data Race

These three terms describe different concurrency problems and are frequently confused.

What it isEffect
Race conditionProgram correctness depends on the timing of concurrent operations on shared stateWrong or inconsistent results (a correctness/safety bug)
Data raceTwo threads access the same memory location concurrently, at least one writes, and there is no synchronization between themUndefined behavior under the memory model; a common cause of race conditions
DeadlockTwo or more threads each hold a resource the other needs and wait foreverNo progress at all (a liveness bug)

A data race is a specific, low-level condition defined by the language memory model, and it is one way to create a race condition — but not the only one. A deadlock is a different failure mode entirely: a race condition produces a wrong answer, while a deadlock produces no answer. You can read more about the locking strategies that, used carelessly, lead to deadlock in the entry on distributed locking in Java.

Race Conditions in Java

Within a single JVM, Java gives you several tools to make a sequence of operations safe.

Synchronization forces threads to take turns:

public synchronized void increment() {
    count++;   // only one thread at a time inside this method
}

Explicit locks such as ReentrantLock do the same thing with finer control over timing, fairness, and interruption.

Atomic classes avoid locking altogether by using a single hardware-level compare-and-set instruction:

private final AtomicLong count = new AtomicLong();

public void increment() {
    count.incrementAndGet();   // atomic read-modify-write
}

Any of these fixes the counter example. For bounded concurrency — limiting how many threads may enter a section at once — Java offers the semaphore, and for making threads wait until a set of operations completes, the CountDownLatch.

There is one critical limitation: all of these mechanisms only work inside a single JVM. synchronized, ReentrantLock, and AtomicLong coordinate threads within one process. They know nothing about other processes — and that is exactly where modern race conditions hide.

Race Conditions in Distributed Systems

Run your service on more than one node — a typical horizontally scaled deployment — and the in-JVM tools stop protecting you. Each instance has its own locks and its own AtomicLong. They cannot see each other.

Now the lost-update bug returns at a larger scale. Suppose two instances both read a value from a shared store, increment it, and write it back:

// Unsafe across nodes: a distributed read-modify-write
long current = store.get("count");
store.put("count", current + 1);

Two instances can read the same value, both increment, and both write back — losing an update, just like two threads did inside one JVM. No amount of local synchronization helps, because the contention is between processes on different machines. Common places this bites in production: decrementing inventory or stock, enforcing a one-time action (a single coupon redemption, a single charge), generating sequential IDs, and updating shared counters.

To coordinate across nodes you need a mechanism that lives in the shared layer the nodes have in common. A distributed lock lets only one node at a time enter a critical section, regardless of which machine it runs on:

RLock lock = redisson.getLock("inventory:item:42");
lock.lock();
try {
    // only one node across the entire cluster runs this at a time
    long current = store.get("count");
    store.put("count", current + 1);
} finally {
    lock.unlock();
}

Better still, push the operation into the shared store so it is atomic by construction — the distributed equivalent of swapping a synchronized block for an AtomicLong:

RAtomicLong count = redisson.getAtomicLong("count");
count.incrementAndGet();   // atomic across the whole cluster

Redisson provides these distributed building blocks as drop-in equivalents of the Java concurrency classes you already know — RLock mirrors ReentrantLock, RAtomicLong mirrors AtomicLong, RSemaphore mirrors Semaphore, and RCountDownLatch mirrors CountDownLatch — so the same patterns that prevent race conditions in one JVM extend cleanly across many. For race conditions specific to enforcing request limits, see the rate limiter.

How to Prevent Race Conditions

A practical checklist, roughly in order of preference:

  • Avoid shared mutable state. Prefer immutable objects and confine data to a single thread where you can. The safest race condition is the one that cannot exist.
  • Make the operation atomic. Use AtomicLong and friends in one JVM, or a distributed atomic such as RAtomicLong across nodes, so the read-modify-write cannot be interrupted.
  • Guard the critical section with a lock. Use synchronized or ReentrantLock within a JVM, and a distributed lock across machines, when several steps must happen together.
  • Limit concurrency where needed. A semaphore caps how many threads or nodes act at once.
  • Design for idempotency. If an operation can safely run more than once with the same effect, a lost or duplicated update stops being a correctness problem.

How to Detect Race Conditions

Race conditions are notoriously hard to catch because they depend on timing that rarely repeats. The bug that never appears on your laptop shows up once a day in production. Useful tactics:

  • Stress and concurrency testing — hammer the suspect code from many threads, repeatedly, ideally on multi-core hardware; tools such as the jcstress harness are built for this.
  • Static and dynamic analysis — thread- and data-race detectors can flag unsynchronized shared access before it ships.
  • Code review focused on shared state — trace every field touched by more than one thread and confirm each access is synchronized, atomic, or confined.

Race Condition: Frequently Asked Questions

Is a Race Condition a Bug?

Almost always, yes. It is a correctness defect: the program can produce a wrong result depending on timing. The rare exceptions are cases deliberately designed to tolerate any interleaving.

Are Race Conditions Always Harmful?

The underlying non-determinism is not harmful by itself — well-designed concurrent systems interleave operations constantly. It becomes a defect when the correctness of a result depends on a particular ordering that the code does not enforce.

What Is the Difference Between a Race Condition and a Deadlock?

A race condition yields a wrong answer because operations interleaved badly. A deadlock yields no answer because threads are stuck waiting on each other. One is a correctness problem; the other is a liveness problem.

Do Race Conditions Only Happen With Multiple Threads?

No. They also occur between separate processes, between nodes in a distributed system, and even within a single process interacting with external state such as files or sockets — anywhere concurrent operations share something mutable.