Race Condition — How to Identify and Fix
Fast recall
Quick revision
Core answer
Whether the candidate can recognise the check-then-act and read-modify-write patterns as race condition triggers.
Fixing only one access point (adding synchronized to the write but not the read, or vice versa), leaving the race condition partially intact.
Mention that races are easiest to find with thread sanitizers or tools like Java PathFinder, Helgrind, or careful code review for non-atomic compound operations.
Interview-ready guide
Interview answer
A race condition means program correctness depends on thread timing that synchronization doesn't control — they pass tests and silently fail in production.
Two main patterns:
- Check-then-act:
containsKey()thenget()— another thread removes between the two calls; fix withcomputeIfAbsent() - Read-modify-write:
counter++reads, increments, writes in three non-atomic steps — two threads can interleave; fix withAtomicInteger.incrementAndGet()
volatile doesn't fix it:
volatileensures visibility but not atomicity —counter++on a volatile int still loses updates under concurrent access
Detection:
jcstress: tests with aggressive scheduling to surface races in controlled environments- SpotBugs: static analysis for unsynchronized field access patterns
- Code review: flag any shared mutable state accessed without synchronization, atomic, or single-writer guarantee
I recommend treating shared mutable state as guilty until proven safe — every access needs synchronization, an atomic operation, or a volatile with a single-writer guarantee.
Race Condition — How to Identify and Fix
Direct Answer
A race condition occurs when a program's correctness depends on the relative timing of thread executions, producing inconsistent results that vary non-deterministically between runs. They arise whenever multiple threads access shared mutable state and at least one access is a write, without proper synchronisation. Fixes include synchronisation, atomic variables, immutable data, or confining state to a single thread.
Deep Dive & Architecture
Six proven techniques, roughly in order of effectiveness: (1) Lock ordering — always acquire locks in the same global order (e.g., by lock identity hash or by a documented precedence).
Eliminates cyclic waits by construction. (2) tryLock(timeout) — use ReentrantLock.tryLock(1, SECONDS) instead of unconditional lock(); if it fails, release everything held and retry. (3) Open calls — never call external methods (callbacks, listeners, other services) while holding a lock; release first. (4) Reduce lock scope — minimize the code inside synchronized blocks; narrow the critical section. (5) Lock-free alternatives — ConcurrentHashMap, AtomicReference, immutable data + atomic reference swap. (6) Single global lock — simple, no cycles possible; fine for low-contention scenarios.
A race condition occurs when a program's correctness depends on the relative timing of thread executions, producing inconsistent results that vary non-deterministically between runs. They arise whenever multiple threads access shared mutable state and at least one access is a write, without proper synchronisation. Fixes include synchronisation, atomic variables, immutable data, or confining state to a single thread.
Common Mistakes & Pitfalls
Fixing only one access point (adding synchronized to the write but not the read, or vice versa), leaving the race condition partially intact.
Check your understanding
Answer these without looking back at the article.
- Explain deadlock, livelock, and starvation in Java concurrency. How do you detect and prevent each?
- How do you detect a deadlock in a running Java application using a thread dump?
- When do Java parallel streams improve performance, and when do they actually hurt it?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.