🧵 Section 9 · Question #21

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:

  1. 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.
  2. 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.
  3. Immutability: Designing immutable domain models (in Java 21, record instances with final fields are ideal). If an object’s state cannot be modified post-instantiation, race conditions are mathematically precluded.
  4. 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:

  1. Initialization in final Fields: JLS §17.5 specifies the “Final Field Freeze”: all final fields are guaranteed to be frozen in memory at the conclusion of constructor execution, provided the this reference did not escape during construction.
  2. Static Initializer Blocks: Guaranteed by the JVM ClassLoader specification (the foundation for the Holder Pattern / Bill Pugh Singleton).
  3. Writing Reference to a volatile Field: Inserts a StoreStore memory barrier, ensuring all preceding field assignments complete before the reference becomes visible.
  4. 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:

  1. 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.
  2. ThreadLocal Confinement: Binding state to ThreadLocal. Always invoke remove() inside a mandatory finally block 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 only StoreLoad reordering. 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:

  1. Immutability: Once bound to an execution scope, a ScopedValue cannot be mutated from within the task, eliminating mutation race conditions.
  2. Bounded Lifetime: Bound strictly to a lexical execution block (where().run(...)), eliminating thread pool memory leaks without requiring manual remove() invocations.
  3. Virtual Thread Optimization: High-density virtual threads share parent scope bindings by reference, drastically reducing heap overhead compared to copying ThreadLocal maps.

Architecture Without Mutable Shared State

Senior engineers avoid race conditions by designing systems that eliminate shared mutable memory:

  1. Actor Model: Computational entities do not share memory; they communicate asynchronously by passing immutable messages into thread-safe mailboxes.
  2. Event Sourcing & CQRS: State transitions are represented as an append-only sequence of immutable events, preventing concurrent in-place row/field mutations.
  3. Copy-On-Write: Containers like CopyOnWriteArrayList optimize 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:

  1. 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.
  2. Guaranteed Single Execution: The provided lambda calculation is executed exactly once for the given key, preventing duplicated instantiation of expensive resources.
  3. 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.