← Software Engineering & Cloud Studio
Concept Explainer · Software Engineering

Race Conditions vs. Deadlocks

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.

The Setup

Same root cause — shared state — two opposite symptoms

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.

Race condition: two threads, one lost increment

Silently Wrong
STARTING VALUEcounter = 5THREAD 1READcounter → 5COMPUTE5 + 1 = 6WRITEcounter = 6THREAD 2READcounter → 5COMPUTE5 + 1 = 6WRITEcounter = 6↑ both threads already read 5 by this point — neither has seen the other's update yetThread 1's write is overwritten a moment laterACTUAL FINAL VALUEcounter = 6Expected 7 after two increments — got 6. One increment vanished.The code ran to completion without an error. It just quietly produced the wrong number.
Expected final value
7
What you'd get if the two increments had run one fully after the other.
Actual final value
6
What you get when both reads happen before either write — a classic lost update.

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.

Deadlock: two threads, one circular wait

Frozen Forever
THREAD AFROZEN — waitingLOCK 1held by Thread ATHREAD BFROZEN — waitingLOCK 2held by Thread Bholdswants — blockedholdswants — blockedcircular waitneither thread will ever get both locks
Threads making progress
0
Both are alive and consuming a thread slot — just permanently blocked.
Locks held vs. needed
1 of 2, each
Each thread holds exactly the lock the other one is waiting for.
Why this works

A race condition is about unsynchronized data access. A deadlock is about lock acquisition order.

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.

Common misconception
"Adding locks around shared data automatically fixes concurrency bugs."

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.

Related Concept Explainers
Concurrency vs. Parallelism — Dealing With Many Things vs. Doing Many Things at Once
Read it →
Lock Ordering & Deadlock Avoidance in Practice
Coming soon

Race Conditions vs. Deadlocks — Concept Explainer

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.

Why This Is Commonly Confused

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.

The Mechanics

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.

Where This Matters In Practice

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).

Frequently asked questions

Can a piece of code have a race condition without ever using a lock?

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.

Does a deadlock ever resolve on its own?

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.

Why does adding a lock to fix a race condition sometimes cause a deadlock instead?

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.

Is a race condition always a bug?

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.)

How do standard deadlock-avoidance techniques work?

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.

Related tools & guides

Concurrency vs. Parallelism — Concept ExplainerStack vs Heap — Concept ExplainerLatency vs. Throughput — Concept ExplainerSoftware Engineering Reference