Java Omnibus
OOAD & Architecture / Concurrency Patterns / Common Threading Bugs
Bugs Threading Java Correctness
Concurrency Patterns

Most threading bugs are not exotic. They come from ordinary code that quietly assumed time would behave like sequence.

Threading bugs are expensive because they are intermittent, environment-sensitive, and often invisible in small tests. The same code may pass ten times and fail on the eleventh run under slightly different timing. This page focuses on the bug families teams actually hit in Java systems and the design habits that reduce them.

Why This Topic Matters

Why it matters in real Java systems

If inventory reservation runs concurrently with cancellation or refund logic, stale reads and races can produce overselling or invalid order states. These failures are rarely caused by one dramatic line of code. They come from unclear ownership, weak visibility guarantees, and hidden timing assumptions.

Core Explanation

The main design idea

The most frequent bug families are race conditions, deadlocks, visibility errors, lost updates, starvation, and misuse of thread pools or blocking calls. Many teams treat them as low-level implementation mistakes, but they are usually design mistakes first. The architecture allowed multiple actors to mutate the same truth without a clear protocol.

A good concurrency design reduces the number of places where timing can matter. It narrows shared mutable state, makes coordination explicit, and keeps thread ownership understandable.

Concept Breakdown

Key ideas to keep in mind

Concept

Race condition

Two or more threads read and write shared state without a stable coordination rule.

Concept

Deadlock

Two paths hold resources in conflicting order and wait forever.

Concept

Visibility bug

One thread updates state and another never sees it reliably because the memory relationship is weak.

Bug families to recognize early

Bug families to recognize early

Correctness

Lost update

Two threads compute from the same old value and one write silently overwrites the other.

Liveness

Deadlock or starvation

The work is logically possible, but the scheduling and locking strategy prevents progress.

Visibility

Stale read

One thread sees an out-of-date view of state and makes a decision that is no longer valid.

Capacity

Pool exhaustion

Blocking work consumes the same executor needed for progress, so throughput collapses.

Java Example

Java example in the Order Management domain

Java example: a lost update on shared stock
package org.javaomnibus.inventory;

public final class StockCounter {
    private int available = 10;

    public void reserveOne() {
        if (available > 0) {
            available = available - 1;
        }
    }
}
Code Walkthrough

Step by step

  1. The code looks innocent in a single-threaded reading.
  2. Under concurrency, two threads can both observe the same positive value and both decrement from it.
  3. The real lesson is not just 'use synchronization.' It is to avoid unclear shared mutation as the default design.
  4. Patterns like Monitor Object or queue ownership exist to make these rules explicit.
Real-World Usage

Where this helps in practice

  • Code review training for multi-threaded services
  • Diagnosing intermittent production failures
  • Teaching why shared mutable state should be treated as high-risk territory
Trade-Offs

Trade-offs and when to be careful

  • Making concurrency safer often means introducing serialization or ownership boundaries that reduce raw parallelism in one place to improve correctness overall.
  • Over-synchronizing everything can create liveness and throughput problems of its own.
Common Mistakes

Frequent mistakes to watch for

  • Assuming tests that pass repeatedly prove thread safety
  • Using synchronized blocks without a coherent lock-order policy
  • Mixing blocking calls into small fixed thread pools
  • Treating visibility bugs as impossible because the code 'obviously writes the value first'
Related Pages

Related concepts


What To Read Next