JAVA Contents

Deadlocks, races, visibility

Understand race conditions, memory visibility, happens-before rules, and deadlock patterns so you can prevent subtle concurrency bugs that only appear under real production load.

On this page

Most concurrency bugs are invisible in development

Concurrency bugs often do not reproduce in local testing. They appear under real production load when timing, CPU scheduling, and memory reordering align unfavorably.

Race condition: not just two threads writing

A race condition occurs when the correctness of the program depends on timing of thread execution.

Classic example

class Counter {
  private int count = 0;

  public void increment() {
    count++; // not atomic
  }
}

count++ is actually three operations:

  • read count
  • add 1
  • write back

Two threads interleaving these steps cause lost updates.

Atomicity vs visibility

  • Atomicity: operation happens indivisibly.
  • Visibility: changes made by one thread are seen by others.

You can have one without the other.

Visibility problem example

class Flag {
  private boolean running = true;

  public void stop() {
    running = false;
  }

  public void loop() {
    while (running) {
      // work
    }
  }
}

Another thread may never see running = false because of CPU caching and instruction reordering.

Enter volatile

private volatile boolean running = true;

volatile guarantees:

  • Writes are visible to other threads.
  • Prevents certain reordering around the variable.

What volatile does NOT do

  • It does not make compound operations atomic.
  • It does not protect invariants across multiple fields.

Incorrect assumption

private volatile int count;

count++; // still not atomic

Use AtomicInteger or synchronization for atomic increments.

Happens-before relationship

Java Memory Model defines happens-before rules that guarantee visibility. Examples:

  • Unlock happens-before subsequent lock on same monitor.
  • Write to volatile happens-before subsequent read of same volatile.
  • Thread start establishes happens-before for started thread.

If no happens-before relationship exists, visibility is not guaranteed.

Safe publication

Publishing an object safely means ensuring other threads see a fully constructed object.

Unsafe publication

class Holder {
  static Holder instance;

  Holder() {
    // complex initialization
  }
}

Another thread may see a partially constructed instance.

Safe publication options

  • Initialize in static initializer.
  • Use volatile.
  • Use final fields correctly.
  • Use synchronized.

Double-checked locking (correct form)

class Singleton {
  private static volatile Singleton instance;

  public static Singleton getInstance() {
    if (instance == null) {
      synchronized (Singleton.class) {
        if (instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

Without volatile, instruction reordering may expose partially constructed instance.

Deadlock basics

Deadlock occurs when threads wait on each other’s locks indefinitely.

Thread dump symptom

  • Threads in BLOCKED state.
  • Each waiting on monitor held by another.

Prevention strategies

  • Enforce global lock ordering.
  • Avoid nested locking.
  • Use tryLock with timeout when appropriate.

Data race example with visibility bug

if (!initialized) {
  initialize(); // another thread might see initialized=true before full setup
}

This can break invariants unless synchronized or volatile is used properly.

Production debugging signals

  • Random null pointer exceptions under load.
  • Inconsistent counters.
  • Threads stuck in BLOCKED or WAITING.
  • State that “sometimes” appears inconsistent.

Testing concurrency safely

Concurrency bugs are timing-sensitive. Stress tests and repeated runs increase probability of exposing race conditions.

Checklist

  • Protect shared mutable state.
  • Understand difference between atomicity and visibility.
  • Use volatile only for visibility, not compound atomicity.
  • Ensure safe publication of shared objects.
  • Enforce consistent lock ordering.
  • Analyze thread dumps during incidents.

Final principle

Concurrency bugs are rarely obvious. They emerge from missing happens-before relationships. Design with explicit synchronization and minimal shared mutable state to avoid production-only failures.