Two different ways concurrent code goes wrong — one quietly hands back the wrong answer, the other never hands back an answer at all.
Both bugs only show up when multiple threads (or processes) are running at once, both are notoriously hard to reproduce, and both get lumped together as "concurrency bugs." But they fail in almost opposite ways. A race condition lets your program finish and return a result — it's just the wrong result, because two threads touched the same data in an order you didn't expect. A deadlockdoesn't return anything at all — the affected threads freeze permanently, each one waiting on a lock the other is holding. Confusing them means reaching for the wrong fix.
A race condition happens when two or more threads access the same shared data at roughly the same time, and at least one of them writes to it. The outcome depends on the exact timing of how their individual read/modify/write steps happen to interleave. Most of the time, the interleaving happens to work out fine. Occasionally — often under load, or only in production, or only once in ten thousand runs — the timing lines up badly and the result is silently wrong, with no crash and no error to point at it. A deadlockhappens for a completely different reason: two or more threads each hold a lock the other one needs, and each refuses to give up its own lock until it gets the other. Neither can ever move forward, because each is waiting on the other in a circle. The program doesn't produce a wrong answer — it just stops, for those threads, forever.
Notice what didn't happen: no exception, no hang, no log line. The program ran exactly as written, on both threads, and finished. It just finished with a counter that's off by one — the kind of bug that passes code review, passes most tests, and only shows up as a slowly drifting number in production under real load. That's the signature of a race condition: a wrong result that completes normally.
A race condition needs nothing more than shared, mutable data and two threads touching it without coordination — no locks involved at all. It produces a result that's wrong but still completes, because every operation in the sequence is still individually able to run; it's only the interleaving of those operations across threads that's incorrect. A deadlock needs the opposite ingredient: locks. Specifically, it needs at least two locks, at least two threads, and those threads acquiring the locks in inconsistent order across different code paths — Thread A grabbing Lock 1 then reaching for Lock 2, while somewhere else Thread B grabs Lock 2 then reaches for Lock 1. Once both threads hold one lock and want the other, nothing about the program can ever resolve that on its own; there's no operation left that's allowed to run. That's the deep irony: the standard fix for a race condition is adding a lock around the shared data. Do that carelessly — with multiple locks acquired in different orders in different places — and you can trade a silently-wrong-but-working program for one that's perfectly correct and permanently frozen. The standard defense is a simple discipline: always acquire multiple locks in the same fixed, global order, everywhere in the codebase, so a cycle like this one can never form.
Half right, and the half that's wrong is the dangerous half. Wrapping shared-data access in a lock so only one thread can read-modify-write it at a time is indeed the standard, correct fix for a race condition — it eliminates the bad interleavings entirely by forcing the two threads' operations to happen one at a time instead of overlapping. But that fix introduces a brand-new hazard the moment a program needs more than one lock. If different parts of the codebase acquire the same two locks in different orders — one code path takes Lock 1 then Lock 2, another takes Lock 2 then Lock 1 — you've built the exact conditions for a deadlock, and it will eventually happen under the right (or wrong) timing, just like a race condition would. Locking correctly means two separate disciplines, not one: synchronizing access to shared data to prevent races, and acquiring every lock in a single, consistent, global order everywhere in the codebase to prevent deadlocks. Adding locks and stopping there only guarantees you've traded one failure mode for the risk of the other.
Explains the two most common ways concurrent code goes wrong: a race condition, where unsynchronized access to shared data lets a program finish but produce a silently incorrect result depending on thread timing; and a deadlock, where a circular wait for locks held by other threads causes the affected threads to hang permanently. Illustrated with a lost-update example on a shared counter and a two-lock circular-wait example.
Both bugs only appear under concurrency, both are timing-dependent and hard to reproduce reliably, and both get called "concurrency bugs" or "threading bugs" without distinction. But they are opposite failure modes. A race condition is a data-correctness problem: the program runs to completion on every affected thread, and the bug is that the final result is wrong. A deadlock is a liveness problem: the program does not run to completion at all for the affected threads, and the bug is that they can never make forward progress again.
A race condition requires only shared, mutable data and at least two threads accessing it without proper synchronization — no locks are even required for the bug to exist. The classic example is a read-modify-write sequence (read a counter, compute a new value, write it back) where two threads both read the old value before either writes the new one, so one thread's update is silently lost.
A deadlock requires locks specifically — at least two locks and at least two threads, where the threads acquire those locks in inconsistent order across different code paths. Thread A acquires Lock 1 and then requests Lock 2; Thread B acquires Lock 2 and then requests Lock 1. Once each thread holds the lock the other one needs, neither can proceed, and no future event will change that on its own — it takes an external intervention such as a timeout, a deadlock detector, or a process restart to break the cycle.
Race conditions show up as data that slowly drifts wrong under real production load — an inventory count that's occasionally off by one, a cache that occasionally serves stale data, a balance that occasionally doesn't reconcile — problems that are notoriously difficult to catch in code review or single-threaded testing. Deadlocks show up as requests, worker threads, or entire services that hang and never return, often traced back to two pieces of code (sometimes written months apart by different engineers) that each lock two shared resources in a different order. The standard defenses are different too: race conditions are prevented with correct synchronization (locks, atomics, or immutable/message-passing designs) around shared data; deadlocks are prevented with a strict, consistent global lock-acquisition order (or by using higher-level constructs like try-locks with timeouts, or avoiding holding multiple locks at once entirely).
Yes — a race condition only requires shared, mutable data and two or more threads accessing it without synchronization. Locks are one common way to fix a race condition, not a prerequisite for having one. Plenty of race conditions occur in code that never uses a lock at all, because no synchronization was added in the first place.
Generally no. Once a circular wait forms — each thread holding a lock another thread needs — nothing in the normal execution of that code changes the situation, since every thread involved is blocked and cannot execute the instructions that would release its lock. Recovery requires something external: a lock-acquisition timeout that forces one thread to give up and retry, a deadlock detector that forcibly kills one of the participants, or a full process/service restart.
Because locking correctly requires two separate disciplines. Adding a lock around shared data does fix the race condition it was added for. But if the codebase already has (or later grows to have) more than one lock, and different code paths acquire those locks in different orders, you've created the exact structure a deadlock needs — a circular wait. The fix for races (synchronize shared data) and the fix for deadlocks (consistent global lock ordering) are both necessary; neither one alone is sufficient.
In the sense engineers usually mean it — an unintended, order-dependent outcome on data that should behave consistently — yes, it's a bug. (The term is sometimes used more loosely for any timing-dependent behavior, including some that are intentional, like a "benign" race where either outcome is acceptable. But when the outcome must be correct regardless of thread timing and isn't, that's the race condition bug this explainer describes.)
The most common technique is a fixed, global lock-ordering convention: if every thread in the codebase that needs both Lock 1 and Lock 2 always acquires Lock 1 first and Lock 2 second — with no exceptions, anywhere — a circular wait between those two locks becomes structurally impossible. Other techniques include using try-lock with a timeout (so a thread that can't get a second lock backs off and releases what it already holds, then retries) and minimizing how often any code path needs to hold more than one lock at a time.
Try our Software Engineering & Cloud Studio
More calculators, simulators, and guides for this discipline.