Data Race vs Race Condition in Java — Memory-Level vs Logical-Level Bugs
Fast recall
Quick revision
Core answer
if (!map.containsKey(k)) map.put(k, v) — both calls are thread-safe individually but the composite is not.Whether the candidate can distinguish memory-level ordering bugs from logical composite-operation bugs, and can give concrete examples of each
Treating the terms as synonyms. Missing that `ConcurrentHashMap.containsKey + put` is a race condition even though both operations are individually thread-safe
Give examples of each: a data race (unsynchronized counter), a race condition without data race (check-then-act on ConcurrentHashMap), a data race without race condition (benign flag read). Mention `putIfAbsent`, `compute`, `computeIfAbsent` as atomic composite alternatives
Interview-ready guide
Interview answer
They're related but not the same — one is a memory-level bug, one is a logic-level bug.
Data race — memory-level concept from the JMM
- Two threads access the same memory location
- At least one access is a write
- No happens-before ordering between them
- The JMM says the result is undefined — stale reads, torn reads, impossible states
- Classic example: unsynchronized
count++across threads
Race condition — logic-level concept about correctness
- The program's correctness depends on accidental interleaving of operations
- Every individual operation can be thread-safe, yet the composite can be wrong
- The JMM has no opinion — this is about program design
The most interview-worthy example: race condition WITHOUT data race
ConcurrentHashMap— every single operation is thread-safeif (!cache.containsKey(id)) { val = load(); cache.put(id, val); }- No data race (all accesses synchronized inside ConcurrentHashMap)
- Yes race condition: two threads both see missing → both load from DB → duplicate work
- Fix:
cache.computeIfAbsent(id, k -> load(k))— atomic check-and-insert
Check-then-act is the family of bugs to watch for
if (!list.contains) list.add— duplicate addsif (balance > 0) withdraw(amount)— overdraftif (instance == null) instance = new...— multiple instancesif (!file.exists()) file.create()— two processes both create- Fix: compound atomic methods like
putIfAbsent,computeIfAbsent,merge,compute, or wrap the whole sequence in a lock
Data race WITHOUT race condition is possible but rare — the benign race
- Unsynchronized read of a flag where the worst outcome is an extra slow-path check
- Works on x86 (strong memory model) but may break on ARM
- Still a bug under strict JMM interpretation
- Always fix with
volatile— cheap and portable
How to detect each
- Data races: static analyzers (Error Prone, SpotBugs), Java Thread Sanitizer (Java 21+), stress tests on ARM
- Race conditions: code review focusing on composite operations, deep correctness testing, formal verification in critical code
Practical fixes in modern Java
- For counters and simple state:
AtomicInteger,AtomicLong,AtomicReference,LongAdder - For maps:
ConcurrentHashMap.computeIfAbsent,merge,compute - For complex state:
synchronized,ReentrantLock, or immutable state +AtomicReference.updateAndGet - For cross-service invariants: database transactions
Recommendation: When reviewing concurrent code, first scan for data races — any field accessed by multiple threads without volatile, synchronized, or an atomic wrapper is suspect. Fix those first with the right synchronization primitive. Then scan for race conditions — anywhere you see containsKey + put, get + set, if (null) create — that's a composite operation that needs to be atomic. Replace with computeIfAbsent, putIfAbsent, or wrap in a lock. Remember: a data-race-free program can still have race conditions, and fixing race conditions often requires atomic composite operations rather than just more synchronization.
Data Race vs Race Condition in Java — Memory-Level vs Logical-Level Bugs
Direct Answer
They're related but distinct. A data race is a memory-level bug: two threads access the same memory location, at least one writes, and there's no happens-before ordering between them. The JMM says the behavior is undefined — you can see stale values, torn reads, or impossible states. A race condition is a logical-level bug: the correctness of your program depends on an accidental interleaving of operations. You can have a race condition WITHOUT a data race (all accesses are synchronized, but the logic is still wrong) and a data race WITHOUT a race condition (benign racy read, though the JMM says it's still undefined behavior). The classic example of race-condition-without-data-race is check-then-act: if (!map.containsKey(k)) map.put(k, v) — both calls are thread-safe individually but the composite is not.
Common Mistakes & Pitfalls
Treating the terms as synonyms. Missing that ConcurrentHashMap.containsKey + put is a race condition even though both operations are individually thread-safe
Check your understanding
Answer these without looking back at the article.
- Why is `ConcurrentHashMap.containsKey + put` a race condition even though both are thread-safe?
- How does `computeIfAbsent` atomically check-and-insert?
- What's a benign data race, and why is it still considered a bug?
- How would you detect a race condition that doesn't involve a data race?
- What's the difference between `putIfAbsent`, `compute`, and `merge`?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.