Java Memory Model: Happens-Before, Volatile, and Final Fields

Understanding happens-before guarantees, volatile field semantics, and final field safety in the Java Memory Model for correct concurrent code.

published: reading time: 27 min read author: GeekWorkBench
Quick Summary

Understanding happens-before guarantees, volatile field semantics, and final field safety in the Java Memory Model for correct concurrent code.

Java Memory Model: Happens-Before, Synchronized, Volatile, and Final Field Semantics

The Java Memory Model sounds like academic fluff until you’re debugging a bug that only appears in production at 3am. The JMM is the set of rules that govern how the JVM handles memory visibility and operation ordering across threads. Get these wrong and you’ll spend weeks chasing data races that vanish when you add logging.

If you’re new to threads, start with Threads and Lightweight Processes for foundational concepts.

Introduction

The Java Memory Model (JMM) is the set of rules that govern how the JVM handles memory visibility and operation ordering across threads. It defines the guarantees that Java programs can rely on when multiple threads access shared memory simultaneously. Without these rules, you would have no reliable way to reason about what one thread sees when another thread writes to a field—the JVM and CPU could reorder operations in ways that produce bugs invisible during testing and catastrophic in production.

The practical consequence of ignoring the JMM is data races: situations where threads disagree on the state of shared data because visibility guarantees are violated. These bugs are notoriously difficult to debug because they often disappear when you add logging (logging introduces synchronization that masks the race), they may only appear under specific CPU architectures or JVM configurations, and they typically resist attempts to reproduce them consistently. The JMM gives you the vocabulary and the tools to identify and fix these issues systematically rather than by trial and error.

This post covers the happens-before relationship that forms the foundation of JMM guarantees, explains how synchronized and volatile establish ordering constraints, and clarifies why final fields have special safety semantics. By the end, you will be able to analyze concurrent code for data races, apply the correct synchronization primitive for each situation, and understand why patterns like double-checked locking require volatile to work correctly. You will also have a clear mental model for reasoning about thread visibility in any Java program you encounter.

What Is the Java Memory Model?

The JMM defines the behavior of memory operations in concurrent Java programs. It specifies how reads and writes to memory happen across threads, ensuring that certain operations appear to happen in a specific order relative to each other.

The core problem the JMM solves is this: without proper synchronization, a thread might see stale data written by another thread, or observe operations in an order that differs from program order. The JMM provides a formal specification for when these visibility guarantees hold.

Modern processors and compilers perform optimizations that can reorder memory operations for performance. The JMM defines a set of rules called happens-before relationships that establish when these reorderings are prohibited.

Happens-Before Relationships

If one action happens-before another, the first action’s results are guaranteed to be visible to the second action. That’s the definition, but let’s look at where these guarantees come from.

Key Happens-Before Rules

Here are the rules that actually matter in practice:

  1. Program Order Rule: Within each thread, each action happens-before any subsequent action in that same thread
  2. Monitor Lock Rule: An unlock of a monitor happens-before each subsequent lock on that same monitor
  3. Volatile Field Rule: A write to a volatile field happens-before each subsequent read of that same field
  4. Thread Start Rule: A call to Thread.start() happens-before any action in the started thread
  5. Thread Termination Rule: A thread termination action happens-before any action that detects the thread has terminated
  6. Transitivity: If A happens-before B, and B happens-before C, then A happens-before C

Practical Example

Here’s a classic data race that trips up a lot of developers:

public class DataRaceExample {
    private int counter = 0;
    private boolean ready = false;

    public void writer() {
        counter = 42;      // Action A
        ready = true;       // Action B
    }

    public void reader() {
        if (ready) {        // Action C
            System.out.println(counter); // Action D
        }
    }
}

Without synchronization, the reader might see counter = 0 even when ready = true. The JMM doesn’t guarantee visibility across threads without happens-before relationships. Making ready volatile fixes this:

private volatile boolean ready = false;

Now the write to ready happens-before the read of ready, ensuring the write to counter is also visible (due to transitivity with the Program Order Rule).

Synchronized Blocks

The synchronized keyword gives you mutual exclusion plus happens-before guarantees. When a thread acquires a monitor and then releases it, all memory operations before the release become visible to the next thread that acquires the same monitor.

Synchronized Method vs Block

public class SynchronizedExample {
    private final Object lock = new Object();
    private int value = 0;

    // Instance method synchronization - uses 'this' as monitor
    public synchronized void increment() {
        value++;
    }

    // Equivalent block syntax
    public void incrementBlock() {
        synchronized (this) {
            value++;
        }
    }

    // Separate lock object for finer control
    public void processWithSeparateLock() {
        synchronized (lock) {
            // Critical section with dedicated lock
            performOperation();
        }
    }
}

Happens-Before Guarantees with Synchronized

When you exit a synchronized block:

  1. All memory operations within the block become visible to the next thread that enters a synchronized block on the same monitor
  2. The monitor unlock action happens-before any subsequent lock action on that same monitor
  3. This applies even if the threads are running on different cores or processors

For more on lock-related issues, see Deadlock and Starvation and Locking and Concurrency.

Common Pitfalls

Over-synchronization is the mistake I run into most. Developers hold a lock across a large collection iteration, a remote call, I/O — anything slow — and suddenly an application that should handle thousands of concurrent requests is bottle-necked to a single thread. The fix sounds simple because it is: keep locks down to the actual mutation, nothing derived and nothing that could block.

Lock ordering deadlocks are worse. Thread A holds Lock 1 and waits for Lock 2 while Thread B holds Lock 2 and waits for Lock 1. Neither makes progress. They surface under load when timing tips the race in the wrong direction. Fixing them requires acquiring multiple locks in a consistent global order — easy to state, harder when those locks span different subsystems or live in libraries you did not write.

Holding locks during blocking operations is the third trap. Calling wait(), sleep(), or performing I/O while holding a monitor lock stalls every other thread that needs that lock for the duration of the blocking call. If enough requests back up behind a thread waiting on a network response while holding the lock, you get thread exhaustion. Keep synchronized blocks short and never do potentially blocking things inside them.

  • Over-synchronization: Holding locks for too long degrades performance and can cause deadlocks
  • Inconsistent lock ordering: Acquiring multiple locks in different orders across threads causes deadlock
  • Holding locks during blocking operations: Never do I/O or wait() while holding a lock

Volatile Fields

Volatile gives you weaker guarantees than synchronized but with less overhead. A write to a volatile field is visible to all subsequent reads of that same field.

When to Use Volatile

Volatile works when:

  • You need visibility guarantees for a single variable across threads
  • The variable is accessed by multiple threads without compound operations
  • You want to signal state changes between threads
public class VolatileExample {
    // Use volatile for flags and state variables
    private volatile boolean shutdownRequested = false;
    private volatile long lastUpdateTime = 0;

    public void requestShutdown() {
        shutdownRequested = true;
    }

    public void doWork() {
        while (!shutdownRequested) {
            // Work loop
            lastUpdateTime = System.nanoTime();
        }
    }
}

When NOT to Use Volatile

Volatile is insufficient when:

  • You need atomic operations on multiple variables
  • You need mutual exclusion
  • Compound operations are required (e.g., counter++)
// BROKEN - counter++ is not atomic even with volatile
private volatile int counter = 0;

// This still has a data race!
public void increment() {
    counter++; // Read-modify-write is not atomic
}

// CORRECT - use AtomicInteger for compound operations
// See also: Lock-Free Data Structures
private AtomicInteger counter = new AtomicInteger(0);

public void increment() {
    counter.incrementAndGet();
}

Volatile and Happens-Before

The volatile field rule is what makes volatile actually useful for cross-thread signaling. When a thread writes to a volatile field, the JVM issues a write memory barrier that drains all pending writes to main memory before the volatile write itself commits. On the read side, a volatile field access triggers a read barrier that invalidates CPU caches, forcing subsequent reads to pull from main memory instead. This barrier pair establishes the happens-before guarantee: any write that precedes a volatile write in program order shows up correctly for any thread that later reads that volatile field.

Transitivity turns this into a chain. Imagine a thread writes to a regular field, then writes to a volatile flag. A second thread reads the volatile flag and sees the new value. That second thread also sees the regular write, even though it never touched the volatile field directly. Program order links the regular write to the volatile write, the volatile field rule links the volatile write to the volatile read, and transitivity stitches the whole chain together. This is exactly how double-checked locking works and why the producer-consumer pattern leans on volatile flags.

The cost difference matters on weakly ordered architectures. x86 and x86-64 have a strong memory model where stores are already ordered, so the JVM mostly skips barriers for volatile. ARM64 is the opposite — the JVM has to emit full fence instructions to guarantee ordering, which is noticeably more expensive. The JMM hides these differences from you, so your code runs the same everywhere, but the hardware underneath does matter.

Final Field Semantics

Final fields get special treatment in the JMM. The JVM guarantees that once a constructor finishes, all threads see the correctly initialized values of final fields.

Why Final Fields Are Special

Without special handling, a partially constructed object could be visible to another thread if a reference to the object escapes before the constructor completes.

public class FinalFieldExample {
    private final int value;
    private final String name;
    private final List<String> items;

    public FinalFieldExample(int value, String name) {
        this.value = value;
        this.name = name;
        this.items = new ArrayList<>();
        items.add("initialized");
    }

    // The reference 'this' should NEVER escape during construction
    // Bad example - DON'T do this:
    // public static FinalFieldExample instance;
    // public FinalFieldExample(int v) {
    //     instance = this; // ESCAPE - 'this' escapes!
    // }
}

Safe Publication Patterns

For safe publication of objects with final fields:

  1. Use final fields: The JVM provides special guarantees
  2. Use static initializer: Static fields are initialized during class loading
  3. Use final reference + final fields: Combining both provides strongest guarantees
  4. Use synchronized or volatile: These establish happens-before guarantees
// Safe publication using static holder pattern
public class SafePublication {
    private final int value;
    private final String description;

    public SafePublication(int value, String description) {
        this.value = value;
        this.description = description;
    }

    // Safe publication via static initializer
    private static final SafePublication INSTANCE = new SafePublication(42, "Meaning of life");

    public static SafePublication getInstance() {
        return INSTANCE;
    }
}

Architecture Diagram

Here’s how the happens-before relationships fit together:

Synchronized Block Happens-Before

When a thread exits a synchronized block, it unlocks the monitor. The monitor lock rule says this unlock happens-before any subsequent lock on the same monitor by another thread. The diagram above shows Thread 1 writing a variable and then unlocking, while Thread 2 locks and reads. Because of the happens-before link between unlock and lock, Thread 2 sees the value Thread 1 wrote, even though the threads never run at the same time.

The guarantee covers every memory operation before the unlock, not just ones touching the locked object. Write to three fields inside the block and all three are visible to the next thread after it acquires the lock. That is why synchronized fixes data races. It also creates a visibility barrier that pushes the whole memory state across to the next thread, not just the fields that happen to be named in the synchronized statement.

graph TB
    subgraph Thread1
        A1[Write to Variable] --> A2[Unlock Monitor]
    end

    subgraph Thread2
        B1[Lock Monitor] --> B2[Read Variable]
    end

    A2 -.->|happens-before| B1

Volatile Field Happens-Before

The volatile field rule is narrower than the monitor lock rule but still useful. A write to a volatile field happens-before every subsequent read of that same field by any thread. The diagram above shows this: Thread 1 writes to a volatile flag, and Thread 2 reads it later. The arrow is the happens-before guarantee carrying write side effects across.

Transitivity is the interesting part. Thread1 writes a regular variable first, then writes to a volatile field. Thread 2 reads the volatile field and sees the regular write too. Program order links the regular write to the volatile write, the volatile field rule links the volatile write to the volatile read, and transitivity closes the chain. This is exactly how double-checked locking works with volatile: the reference write is ordered before the volatile write that publishes it, so any thread reading the reference through a volatile read also sees the fully constructed object.

graph TB
    subgraph VolatileExample
        C1[Write to Volatile] --> C2[Read Volatile]
    end

    C1 -.->|happens-before| C2

Final Fields Safe Publication

Final fields play by different rules. The JMM guarantees that once a constructor finishes and the reference to the object escapes through a safe mechanism, all final fields are visible in their correctly initialized states to every thread. The diagram above shows this: the constructor completes, then the reference is published, and those two steps together constitute safe publication.

Safe publication means the reference escapes in one of a few specific ways: a static field initializer, a final field, a synchronized block, or a volatile field. When the reference goes out through any of these, the JVM plants a memory barrier that makes all final field writes visible. Without safe publication, even final fields can show up as their default zero values if the reference escapes early through a this-escape bug during construction.

graph TB
    subgraph FinalFields
        D1[Constructor Completes] --> D2[Reference Published]
    end

    D1 -.->|safe publication| D2

Production Failure Scenarios

Scenario 1: Double-Checked Locking with Non-Volatile Reference

// BROKEN - classic double-checked locking failure
public class BrokenSingleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                 // First check
            synchronized (BrokenSingleton.class) {
                if (instance == null) {         // Second check
                    instance = new Singleton(); // Problem here!
                }
            }
        }
        return instance;
    }
}

Failure: The instance = new Singleton() operation involves multiple steps (allocate memory, call constructor, assign reference). Without volatile, another thread might see a non-null instance but with uninitialized fields.

Fix: Make instance volatile:

private static volatile Singleton instance;

Scenario 2: Publication via Constructor Argument

// BROKEN - unsafe publication
public class UnsafePublisher {
    private final int value;

    public UnsafePublisher(Consumer<Integer> consumer) {
        this.value = computeValue();
        consumer.accept(this.value); // 'this' escapes!
    }
}

Failure: The this reference escapes during construction, allowing another thread to observe the object in a partially constructed state.

Fix: Never pass this to another object during construction.

Scenario 3: Volatile for State Machine Without Atomic Transitions

// BROKEN - state transitions are not atomic
private volatile int state = 0;

public void transition() {
    if (state == 0) {
        // Another thread might also enter this block
        state = 1; // Race condition
        performTransition();
    }
}

Fix: Use synchronized or AtomicInteger for compound operations.

Trade-off Table

ApproachVisibilityAtomicityPerformanceUse When
No synchronizationNone guaranteedN/ABestSingle-threaded only
synchronizedAll memory opsMutual exclusionModerate overheadMutual exclusion needed
volatileAll memory opsSingle variableLow overheadFlag/state signals
Atomic* classesAll memory opsSingle operationLow overheadCounter, flags, references
final fieldsSafe constructionImmutabilityNo overheadConstants, immutable data

Implementation Snippets

Safe Counter with Multiple Fields

public class SafeCounter {
    private final Object lock = new Object();
    private long count = 0;
    private long lastUpdate = 0;

    public void increment() {
        synchronized (lock) {
            count++;
            lastUpdate = System.nanoTime();
        }
    }

    public long getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

Flag-Based Shutdown with Volatile

public class VolatileShutdownFlag {
    private volatile boolean shuttingDown = false;

    public void initiateShutdown() {
        shuttingDown = true;
    }

    public boolean isShuttingDown() {
        return shuttingDown;
    }

    // Use in work loop
    public void runLoop() {
        while (!shuttingDown) {
            processNextTask();
        }
        cleanup();
    }
}

Safe Listener Registration

public class SafeListenerRegistration {
    private final List<Listener> listeners = new ArrayList<>();

    // Safe - no 'this' escape
    public void registerListener(Listener listener) {
        synchronized (listeners) {
            listeners.add(listener);
        }
    }

    // Safe publication via final field + synchronized access
    private final List<Listener> listenersCopy;

    public SafeListenerRegistration() {
        List<Listener> temp = new ArrayList<>();
        // populate temp
        this.listenersCopy = Collections.unmodifiableList(temp);
    }

    public List<Listener> getListeners() {
        return listenersCopy;
    }
}

Observability Checklist

When debugging concurrency issues related to the JMM:

  • Is the field volatile if it accessed across threads without synchronization?
  • Are all shared mutable fields properly synchronized or volatile?
  • Does any code hold locks for longer than necessary?
  • Is there a potential for lock ordering deadlocks?
  • Are final fields being properly initialized before publication?
  • Is ‘this’ reference escaping during construction?
  • Are atomic operations truly independent (no read-modify-write races)?
  • Can you verify happens-before relationships in your code paths?

Security Notes

Concurrency bugs can become security vulnerabilities:

  • Time-of-check to time-of-use (TOCTOU): Checking a condition and acting on it is not atomic
  • Uninitialized data exposure: Final field semantics prevent this if used correctly
  • Order confusion attacks: Without happens-before guarantees, an attacker might observe incorrect operation ordering
  • Side-channel timing attacks: Lock contention timing can leak information about sensitive operations

Always use secure coding practices combined with proper synchronization.

Common Pitfalls / Anti-Patterns

  1. Assuming visibility without synchronization: A write in one thread may never be visible to another without happens-before guarantees
  2. Using volatile for compound operations: volatile does not make i++ atomic
  3. Nested locks on different objects: Can cause deadlocks if lock ordering differs across threads
  4. Returning mutable objects from getters: Always return copies or immutable views
  5. Starting threads in constructors: This escapes this before the object is fully constructed
  6. Ignoring exception handling in concurrent code: Exceptions can leave locks in unexpected states

Quick Recap Checklist

  • Happens-before guarantees ensure memory visibility across threads
  • Synchronized provides mutual exclusion and happens-before for all memory operations
  • Volatile ensures writes are visible to subsequent reads of the same field
  • Final fields provide safety guarantees when properly constructed
  • Never let this escape during construction
  • Use atomic classes for compound operations on counters and flags
  • Double-checked locking requires volatile on the reference
  • Transitivity extends happens-before across thread boundaries

Interview Questions

1. What is the difference between synchronized and volatile in Java?

Synchronized provides both mutual exclusion and happens-before guarantees for all memory operations within the synchronized block. It ensures that only one thread executes the critical section at a time and that all memory changes are visible when entering and exiting the synchronized block.

Volatile provides weaker guarantees - only ensuring that writes to the volatile field are visible to subsequent reads of the same field. It does not provide atomicity for compound operations. Volatile has lower overhead than synchronized and never causes thread blocking.

2. Explain the happens-before relationship and why it matters in concurrent Java code.

A happens-before relationship guarantees that if action A happens-before action B, then the results of A are visible to B. This is critical because modern JVMs and CPUs can reorder memory operations for performance. Without happens-before guarantees, a thread might see stale data or operations in an unexpected order.

The JMM defines specific rules: program order within a thread, monitor lock/unlock ordering, volatile read/write ordering, thread start/join ordering, and transitivity. These rules allow developers to reason about correctness in concurrent programs.

3. Why is double-checked locking broken without volatile?

Double-checked locking attempts to reduce synchronization overhead for lazy initialization. However, without volatile on the instance field, the JVM can reorder the assignment of the reference with the execution of the constructor. A thread might see a non-null reference while the object's fields are still at their default values (zero, null, false).

Making the instance field volatile prevents this reordering by establishing a happens-before relationship between the write to the field and subsequent reads, ensuring the constructor has fully completed before the reference becomes visible.

4. How do final fields provide safety guarantees in concurrent Java programs?

Final fields have special semantics that prevent observation of partially constructed objects. The JVM ensures that once a constructor containing final field writes completes, and the reference to the object is published in a safe way (such as through a final field, static initializer, or properly synchronized access), all threads will see the correctly initialized values of those final fields.

This prevents a common concurrency bug where another thread might see default values for fields set in the constructor, even though the constructor has supposedly completed. The key is avoiding 'this' escape during construction.

5. What is the 'this' escape issue and how do you prevent it?

'This' escape occurs when a reference to the object being constructed becomes visible to another thread before the constructor finishes executing. This typically happens by publishing the reference in a callback, starting a thread, or adding 'this' to a collection during construction.

Prevention strategies include: never register listeners or callbacks with 'this' during construction, avoid starting threads in constructors, use static factory methods or holder patterns for safe publication, and use final fields combined with proper initialization. The fix is to complete all initialization before publishing the reference.

6. What is the happens-before order guarantee provided by volatile fields?

A write to a volatile field happens-before every subsequent read of that same field. This means any variables written before the volatile write are guaranteed to be visible to reads that occur after the volatile read due to transitivity with the program order rule.

For example, if you write ready = true as volatile after writing data = compute(), any thread that reads ready as true is also guaranteed to see the computed data value.

7. How does the Java Memory Model handle reordering by the JVM and CPU?

The JMM permits reordering of memory operations for performance, but defines happens-before rules that prohibit certain reorderings. Within a single thread, program order is preserved. Across threads, happens-before relationships from locks, volatile, and thread start/join establish ordering constraints.

The key insight is that without synchronized or volatile, the JVM and CPU can legally reorder operations in ways that produce seemingly impossible behavior in concurrent code. The JMM formalizes when visibility is guaranteed.

8. Can you explain the relationship between synchronized and the happens-before relationship?

Synchronized establishes happens-before through the monitor lock rule: unlocking a monitor happens-before subsequent locking of the same monitor. This means all memory operations in a synchronized block become visible to the next thread that enters a synchronized block on the same monitor.

The unlock happens-before the lock, and combined with program order within each thread, this chains visibility guarantees across thread boundaries. This is why synchronized is so effective for correcting data races.

9. What is the "double-checked locking" idiom and why does it require volatile?

Double-checked locking attempts to reduce synchronization overhead for lazy singleton initialization. The first check without locking determines if initialization is needed; the second check inside synchronized prevents race conditions. Without volatile, the reference assignment can be reordered relative to constructor execution.

A thread might see a non-null reference but fields set to default values (zero, null, false) because the JVM assigned the reference before the constructor finished. Volatile prevents this reordering by establishing happens-before between the write and subsequent reads.

10. How do final fields interact with the Java Memory Model after construction?

The JMM provides special guarantees for final fields. Once a constructor finishes and the reference to the object is published through a final field, static initializer, or properly synchronized access, all threads are guaranteed to see the correctly initialized values of final fields—not their default values.

This prevents observation of partially constructed objects containing final fields. However, this guarantee requires that the reference does not escape during construction (no 'this' escape).

11. What is a data race and how does the Java Memory Model define it?

A data race occurs when there are multiple concurrent accesses to the same variable, at least one is a write, and the accesses are not ordered by happens-before relationships. The JMM states that a program has a data race if the behavior cannot be proven by considering only the program order and monitor lock rules.

The practical consequence is that without proper synchronization, memory operations can be reordered and visibility is not guaranteed, leading to unpredictable behavior. Synchronized, volatile, or other concurrency primitives establish the necessary ordering.

12. Explain the monitor lock rule in the Java Memory Model.

The monitor lock rule states that an unlock operation on a monitor happens-before every subsequent lock operation on the same monitor. This means when thread A unlocks a monitor and thread B later locks the same monitor, all memory operations performed by thread A before the unlock become visible to thread B after it acquires the lock.

This rule applies to both synchronized methods (which use the implicit monitor of the object) and synchronized blocks with explicit lock objects. The guarantee extends to all memory operations, not just those touching the locked object.

13. Can volatile variables be used to implement atomic operations like increment?

No. Volatile only guarantees visibility of individual reads and writes. Operations like counter++ involve three steps: read the current value, increment it, and write the new value. This read-modify-write sequence is not atomic even with volatile.

If two threads both read counter = 0 simultaneously, both might increment to 1 and write it back, resulting in lost updates. For atomic increment, use AtomicInteger.incrementAndGet() or synchronize the entire operation.

14. What is the relationship between the Thread Start rule and happens-before?

The Thread Start rule states that a call to Thread.start() happens-before any action in the started thread. This means all memory operations performed by the calling thread before start() are guaranteed to be visible to the new thread when it begins execution.

For example, if you initialize fields before calling start(), the started thread will see the initialized values, not default values. This rule ensures proper initialization visibility across thread boundaries.

15. How does the Java Memory Model handle the Thread Termination rule?

The Thread Termination rule states that a thread's termination action happens-before any code that detects the thread has terminated (via join(), isAlive(), or other means). When thread A calls threadB.join() and it returns normally, all memory operations performed by thread B are visible to thread A.

This rule is critical for safe result propagation: when a worker thread completes and you call join(), you are guaranteed to see all the work that thread accomplished, not just the final state.

16. What is the transitivity rule in happens-before relationships?

The transitivity rule states that if action A happens-before B, and B happens-before C, then A happens-before C. This chains together multiple happens-before relationships to establish ordering across complex synchronization patterns.

For instance, if a volatile write happens-before an unlock (through program order), and that unlock happens-before a subsequent lock (monitor rule), then the volatile write happens-before the lock acquisition, establishing visibility of the written value.

17. Why is it important to restore the interrupt flag after catching InterruptedException?

The interrupt flag represents a signal sent by another thread. When InterruptedException is thrown, the JVM automatically clears the interrupt flag. If you catch the exception without restoring the flag, the interrupt signal is lost forever.

Restoring the flag with Thread.currentThread().interrupt() preserves the signal so code higher up the call stack or in other threads can detect that an interrupt was requested. Failing to restore means the interrupt is silently swallowed, potentially causing threads to ignore shutdown requests.

18. What is the difference between the program order rule and the volatile field rule?

The program order rule applies within a single thread: each action happens-before any subsequent action in that same thread. This means reordering within a thread is constrained by source code order.

The volatile field rule applies across threads: a write to a volatile field happens-before every subsequent read of that same field. This rule specifically constrains visibility across threads for the volatile variable, not all operations in the thread.

Together, the program order rule within a writer thread and the volatile field rule at the reader thread create a happens-before chain that ensures the write is visible.

19. How does safe publication through a static initializer work with final fields?

Static initialization is performed by the JVM under a special lock that guarantees synchronized access. When a static field is initialized in a static initializer or assigned directly, the reference to the enclosing class is published in a way that establishes happens-before for all threads that access the field.

Combining this with final fields means: once the static holder instance is created, all final fields within it are guaranteed to be visible to any thread that accesses the instance through the static getter, without additional synchronization.

20. What happens if you call start() twice on the same Thread object?

Calling start() twice on the same Thread throws IllegalThreadStateException. After a thread has been started and reached TERMINATED state, it cannot be restarted. The JVM tracks the thread's state and enforces this constraint.

This is why creating a "restartable" thread pattern requires wrapping: either use an executor service that manages thread lifecycle, or implement a loop inside run() that processes multiple tasks without the thread ever terminating.

Further Reading

Conclusion

You now understand happens-before guarantees. Apply this knowledge to ensure your concurrent code is correct: use volatile for simple flags, synchronized for compound operations, and never let this escape during construction. When designing concurrent classes, sketch out the happens-before chain to verify that writes in one thread are visible to reads in another. Continue with Thread Lifecycle: States, start, yield, sleep, interrupt, join to understand how thread state transitions interact with the JMM.

Category

Related Posts

Java Thread Lifecycle: States, start, yield, sleep, interrupt, join

Understanding Java thread lifecycle management: thread states, start, yield, sleep, interrupt, join, daemon threads and common pitfalls.

#java #jvm #concurrency

Java Atomics and VarHandle: Low-Level Concurrency

Understanding Java atomic operations: AtomicInteger, AtomicReference, VarHandle, compareAndSet, atomics vs locks, and lock-free programming patterns.

#java #jvm #concurrency

Java Concurrent Collections: ConcurrentHashMap, BlockingQueue

Java concurrent collections deep dive: ConcurrentHashMap, BlockingQueue, CopyOnWriteArrayList, ConcurrentLinkedQueue, and choosing the right structure.

#java #jvm #concurrency