Back to Race Conditions
Q2Question 2 of 3

Race condition vs deadlock?

Key Concepts
  • A race condition produces wrong data but the program keeps running (silent corruption).
  • A deadlock produces no data: the program freezes permanently (visible outage).
  • Race conditions are harder to detect because they are non-deterministic and intermittent.
  • Deadlocks are easier to detect: jstack shows 'Found one Java-level deadlock' immediately.
  • Race conditions are arguably worse in banking: silent balance corruption vs a visible service freeze.
  • Fixing race conditions by adding locks can introduce deadlocks if lock ordering is not consistent.

Definition

A race condition produces wrong data while the program keeps running, while a deadlock freezes the program permanently with no progress.
  • Race condition – shared mutable state accessed without synchronization produces wrong results. The program continues running but the data is corrupted. Non-deterministic: works most of the time, fails under load.
  • Deadlock – threads permanently block each other waiting for locks. No progress is made. The program freezes. Deterministic once triggered: the threads are stuck forever.
  • Detection difference – race conditions are hard to detect (intermittent, silent corruption discovered during audits). Deadlocks are easy to detect (jstack immediately shows the deadlock chain).
  • Danger comparison – race conditions are arguably more dangerous: silent balance corruption in a bank is worse than a visible service freeze that triggers immediate alerts.

A race condition is like two tellers updating Rajesh's ledger without coordinating: the ledger shows Rs.50,000 instead of Rs.0. The bank keeps running, but the books are wrong. Nobody notices until the audit. A deadlock is like two tellers each holding a key the other needs: both stand frozen at the vault door forever. The bank stops operating entirely. Customers complain immediately.

Understanding the Concepts

How do race conditions and deadlocks differ?

| Feature | Race Condition | Deadlock |
|---|---|---|
| Symptom | Wrong data, lost updates | Program freeze, no progress |
| Program runs? | Yes (with wrong results) | No (stuck forever) |
| Deterministic? | No (timing-dependent) | Yes (once triggered, permanent) |
| Detection | Hard (intermittent, silent) | Easy (jstack, ThreadMXBean) |
| Cause | Missing synchronization | Circular lock dependency |
| Fix | Add sync, Atomic, volatile | Lock ordering, tryLock |
| Severity in banking | Silent balance corruption | Visible service outage |

What does a race condition look like in practice?

JAVA
package com.bank.core;

public class RaceInPractice {
    private static int balance = 100000;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> { balance -= 60000; });
        Thread t2 = new Thread(() -> { balance -= 60000; });
        t1.start(); t2.start();
        t1.join();  t2.join();
        // Program continues, but balance is wrong!
        System.out.println("Balance: " + balance);
        // Could be 40000, -20000, or 100000 depending on timing
    }
}

What does a deadlock look like in practice?

JAVA
package com.bank.core;

public class DeadlockInPractice {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lockA) {
                try { Thread.sleep(50); } catch (InterruptedException e) { }
                synchronized (lockB) { System.out.println("T1 done"); }
            }
        }).start();
        new Thread(() -> {
            synchronized (lockB) {
                try { Thread.sleep(50); } catch (InterruptedException e) { }
                synchronized (lockA) { System.out.println("T2 done"); }
            }
        }).start();
        // Program hangs. Neither "T1 done" nor "T2 done" is printed.
    }
}

Can fixing one introduce the other?

Yes. Adding locks to fix a race condition can introduce a deadlock if the locks are acquired in inconsistent order.

JAVA
package com.bank.core;

public class FixingOneCreatesOther {
    // Step 1: race condition (no locks)
    // balance -= amount; // both threads read same value

    // Step 2: add locks to fix race (but wrong order = deadlock)
    // Thread-1: lock(A) then lock(B)
    // Thread-2: lock(B) then lock(A) -> DEADLOCK!

    // Step 3: consistent order fixes both
    // Thread-1: lock(A) then lock(B)
    // Thread-2: lock(A) then lock(B) -> SAFE
}

What is a livelock?

A livelock is a variation where threads are running (not blocked) but making no progress. They keep retrying an operation that keeps failing.

TEXT
Thread-1: tryLock(A) -> tryLock(B) fails -> release A -> retry
Thread-2: tryLock(B) -> tryLock(A) fails -> release B -> retry
Both keep retrying forever: active but no progress.
Fix: add random backoff between retries.

What about starvation?

Starvation occurs when a thread never gets to run because other threads continually acquire the lock first. A non-fair lock allows barging: a newly arriving thread can acquire the lock before threads that have been waiting longer. Fix: use new ReentrantLock(true) for fair ordering.

What should a senior engineer know?

  • Race conditions corrupt data silently: in a banking system, a race condition in the balance update could go unnoticed for days until an audit reveals discrepancies. A deadlock triggers immediate alerts because the service stops responding.
  • Thread-safety testing: use jcstress (OpenJDK concurrency stress test harness) to systematically test for race conditions. Standard JUnit tests rarely trigger timing-dependent bugs.
  • Production monitoring: monitor both thread pool saturation (potential deadlock sign) and data consistency checks (potential race condition sign).

Visual Overview

DIAGRAM
  Race Condition:
  ┌──────────┐     wrong data     ┌──────────────┐
  │ Thread-1  │────────────────────▶│  Corrupted    │
  │ Thread-2  │  both run, both    │  Balance      │
  │ (no sync) │  modify shared     │  (silent bug) │
  └──────────┘  state              └──────────────┘
  Program continues running with wrong results.

  Deadlock:
  ┌──────────┐     permanent       ┌──────────────┐
  │ Thread-1  │─── freeze ─────────▶│  No Progress  │
  │ Thread-2  │  each holds lock   │  Service Down │
  │ (circular)│  other needs       │  (loud alarm) │
  └──────────┘                     └──────────────┘
  Program stops. Threads stuck forever.

Key Takeaway

Race conditions and deadlocks are both concurrency bugs with fundamentally different symptoms. A race condition produces wrong data while the program keeps running. It is non-deterministic, often works correctly, and fails only under specific timing. A deadlock produces no data: the program freezes permanently. It is easier to detect because jstack immediately identifies it.

Race conditions are arguably more dangerous in banking systems because silent balance corruption can go undetected for days. A deadlock causes an immediate service outage that triggers alerts and gets fixed quickly. Both can coexist: adding locks to fix a race can introduce a deadlock if lock ordering is not consistent.

Remember one sentence: a race condition silently corrupts data while the program runs, while a deadlock visibly freezes the program, and fixing one can introduce the other if you are not careful.

---

Share
Use ← → arrow keys to navigate