How to avoid race condition
To prevent and eliminate Race Conditions in Java, developers rely on four foundational architectural strategies:
🟢 Junior Level
30-Second Summary
To prevent and eliminate Race Conditions in Java, developers rely on four foundational architectural strategies:
- Mutual Exclusion: Guarding critical sections using lock primitives (
synchronized,ReentrantLock), guaranteeing that compound operations are executed by at most one thread at any given instant. - Lock-Free Atomic Primitives: Utilizing atomic classes from
java.util.concurrent.atomic(AtomicInteger,AtomicReference,LongAdder) that leverage hardware-assisted CPU CAS (Compare-And-Swap) instructions. - Immutability: Designing immutable domain models (in Java 21,
recordinstances withfinalfields are ideal). If an object’s state cannot be modified post-instantiation, race conditions are mathematically precluded. - Thread Confinement: Eliminating shared memory across threads by isolating state to method stack frames or utilizing
ThreadLocal/ScopedValue.
Strategy Comparison Overview
| Strategy | Java 21 Primitives | Advantages | Trade-offs |
|---|---|---|---|
| Pessimistic Locking | synchronized, ReentrantLock |
Protects arbitrary multi-step transactions | Context switch overhead, deadlock risk |
| Optimistic Lock-Free | AtomicInteger, LongAdder |
Maximum performance (~5–10 ns), immune to deadlocks | Applicable only to single variables |
| Immutability | record, Collections.unmodifiable* |
Inherent thread safety out of the box | GC allocation pressure when creating modified copies |
| Thread Confinement | Method stack, ScopedValue |
Zero memory contention, 100% isolation | Requires careful lifecycle management |
Java 21 Code Examples
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ConcurrentHashMap;
public class RaceConditionPreventionDemo {
// 1. Lock-free Atomic counter
public static class SafeCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }
public int get() { return count.get(); }
}
// 2. Immutability via Java 21 Record
public record UserProfile(long id, String name, String email) {
// Compact validation constructor
public UserProfile {
if (name == null) throw new IllegalArgumentException("Name cannot be null");
}
}
// 3. Check-Then-Act elimination via ConcurrentHashMap
public static class CacheService {
private final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
public String getOrCompute(String key) {
// Atomic check-and-insert: mapping function executes exactly once!
return cache.computeIfAbsent(key, k -> "ComputedValueFor_" + k);
}
}
}
🟡 Middle Level
Safe Publication of Objects
A subtle manifestation of a Race Condition is a Publication Race, where an observer thread obtains a reference to a newly instantiated object before its constructor has completely committed its field values to main memory (observing a partially initialized object).
4 Guaranteed Safe Publication Mechanisms Under the JMM:
- Initialization in
finalFields: JLS §17.5 specifies the “Final Field Freeze”: allfinalfields are guaranteed to be frozen in memory at the conclusion of constructor execution, provided thethisreference did not escape during construction. - Static Initializer Blocks: Guaranteed by the JVM ClassLoader specification (the foundation for the Holder Pattern / Bill Pugh Singleton).
- Writing Reference to a
volatileField: Inserts aStoreStorememory barrier, ensuring all preceding field assignments complete before the reference becomes visible. - Acquiring Monitor Locks: Establishing an explicit Happens-Before edge by synchronizing object publishing and consumption blocks.
// Thread-safe lazy initialization without synchronization overhead (Holder pattern):
public class GlobalServiceRegistry {
private GlobalServiceRegistry() {}
private static class Holder {
// The JVM ClassLoader guarantees thread-safe, atomic initialization upon first reference!
private static final GlobalServiceRegistry INSTANCE = new GlobalServiceRegistry();
}
public static GlobalServiceRegistry getInstance() {
return Holder.INSTANCE;
}
}
Atomic Compound Operations on Collections
A common mid-level defect is invoking independent operations on a thread-safe container:
// HAZARDOUS: Concurrent threads can evaluate containsKey simultaneously!
if (!map.containsKey(key)) {
map.put(key, value);
}
// SOLUTION 1: Atomic insertion if absent
map.putIfAbsent(key, value);
// SOLUTION 2: Atomic lazy evaluation (ideal for expensive objects)
map.computeIfAbsent(key, k -> createExpensiveResource(k));
// SOLUTION 3: Atomic reduction/aggregation
map.merge(word, 1, Integer::sum);
Thread Confinement Techniques
When state is isolated to a single thread, synchronization is unnecessary:
- Stack Confinement: Local primitive and reference variables allocated inside a method exist exclusively within the executing thread’s private stack frame and cannot be accessed by other threads.
- ThreadLocal Confinement: Binding state to
ThreadLocal. Always invokeremove()inside a mandatoryfinallyblock when interacting with pooled threads to avoid Metaspace and memory leaks.
🔴 Senior Level
Hardware Architecture Impact: ARM64 vs. Intel x86
Concurrency bugs that remain dormant during local development on Intel x86 developer machines frequently explode when deployed to cloud production environments running ARM64 architectures (AWS Graviton, Apple Silicon, Ampere).
Hardware Memory Models:
- x86 (TSO — Total Store Order): A strongly ordered hardware memory model. The CPU enforces almost all execution ordering (
StoreStore,LoadLoad,LoadStore), permitting onlyStoreLoadreordering. Flawed, unsynchronized code often runs without apparent error on x86 because the hardware masks missing memory fences. - ARM64 (Weak Memory Ordering): A weakly ordered memory model. ARM cores aggressively reorder independent load and store operations to maximize instruction pipeline throughput. Unsynchronized Java code deployed to ARM immediately reads stale values, uninitialized fields, and corrupted pointers.
Java 21 Scoped Values (JEP 446 / JEP 429)
Java 21 introduces ScopedValue, engineered specifically to supersede ThreadLocal for massive-scale virtual thread workloads:
public class SecurityContextHolder {
// Immutable context value bound to an execution scope
public static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
public void processRequest(User user) {
// Context is safely and efficiently bound across invocation scopes:
ScopedValue.where(CURRENT_USER, user).run(() -> {
doStep1();
doStep2();
}); // Automatically unbound and cleared upon scope exit!
}
private void doStep1() {
User user = CURRENT_USER.get();
}
}
Advantages Over ThreadLocal:
- Immutability: Once bound to an execution scope, a
ScopedValuecannot be mutated from within the task, eliminating mutation race conditions. - Bounded Lifetime: Bound strictly to a lexical execution block (
where().run(...)), eliminating thread pool memory leaks without requiring manualremove()invocations. - Virtual Thread Optimization: High-density virtual threads share parent scope bindings by reference, drastically reducing heap overhead compared to copying
ThreadLocalmaps.
Architecture Without Mutable Shared State
Senior engineers avoid race conditions by designing systems that eliminate shared mutable memory:
- Actor Model: Computational entities do not share memory; they communicate asynchronously by passing immutable messages into thread-safe mailboxes.
- Event Sourcing & CQRS: State transitions are represented as an append-only sequence of immutable events, preventing concurrent in-place row/field mutations.
- Copy-On-Write: Containers like
CopyOnWriteArrayListoptimize for 99% non-blocking reads and 1% writes by cloning the underlying array on mutation.
🎯 Interview Cheat Sheet
1. 30-Second Elevator Pitch
Race Conditions are prevented via four pillars: Mutual Exclusion (synchronized/ReentrantLock), Lock-Free Atomics (Atomic*, LongAdder based on CPU CAS), Immutability (record with final fields), and Thread Confinement (Stack variables, ThreadLocal, and Java 21 ScopedValue). When utilizing ConcurrentHashMap, always leverage atomic compound methods (computeIfAbsent, merge) rather than checking state before mutating. Note that weakly ordered ARM64 architectures (AWS Graviton) expose unsynchronized data races far more aggressively than x86.
2. 4 Tricky Follow-up Questions with Deep Answers
Question 1: Why does the final modifier guarantee thread-safe publication, and under what condition is this guarantee voided?
Answer:
Under JSR-133 and JLS §17.5, the JVM enforces the “Final Field Freeze” rule. When an object constructor completes, a StoreStore memory barrier is executed, flushing all final fields from CPU store buffers to cache/memory before the reference to the new instance is made visible to other threads. Readers are guaranteed to see initialized values even without synchronization.
Condition Voiding Guarantee: If the this reference “escapes” the constructor before it finishes (e.g., passing this to a listener or spawning a thread inside the constructor), the JMM guarantees are completely invalidated, and other threads may observe null or default field values.
Question 2: Why is ConcurrentHashMap.computeIfAbsent() superior to get() + synchronized + put()?
Answer:
- Fine-Grained Concurrency:
computeIfAbsent()locks solely the target hash bucket (using the first node monitor), leaving all other buckets accessible for parallel reads and writes. - Guaranteed Single Execution: The provided lambda calculation is executed exactly once for the given key, preventing duplicated instantiation of expensive resources.
- Atomic Compound Operation: It combines existence verification and insertion into an indivisible operation, completely eliminating the Check-Then-Act race condition.
Question 3: Why do race conditions manifest far more frequently on ARM64 processors than on Intel x86?
Answer: Intel x86 utilizes Total Store Order (TSO), where hardware memory controllers preserve the order of almost all memory reads and writes, masking missing volatile barriers or race conditions. ARM64 employs Weak Memory Ordering, where hardware cores reorder independent reads and writes aggressively to saturate superscalar execution units. Code missing proper memory barriers fails immediately on ARM64.
Question 4: How does Java 21 ScopedValue eliminate the memory leak risks inherent to ThreadLocal?
Answer:
ThreadLocal instances live in a thread’s internal map until explicitly removed. In long-lived thread pools, omitting remove() leaks memory and causes cross-request context bleeding. ScopedValue is lexically scoped: it is bound via ScopedValue.where(KEY, value).run(...) and is automatically collected when the lambda exits. Furthermore, ScopedValue is immutable, eliminating mutation race conditions across nested virtual threads.
3. Red Flags & Common Gotchas
❌ “Wrapping a list in Collections.synchronizedList() makes for-each loops thread-safe.”
Correction: Individual methods are synchronized, but iteration across the list must be explicitly guarded with synchronized(list) to avoid ConcurrentModificationException.
❌ “Allowing this to escape the constructor when final fields are present.”
Correction: Publishing this before the constructor returns breaks the JMM safe publication contract.
❌ “Using CopyOnWriteArrayList in write-heavy workloads.”
Correction: Every mutation clones the entire backing array (Arrays.copyOf), causing severe GC churn when writes are frequent.