The JVM’s memory model doesn’t actually guarantee that a write in one thread is visible to another thread immediately, even if you use volatile or locks.

Let’s watch it work. Imagine two threads, Thread A and Thread B.

// Shared mutable state
volatile int value = 0;
boolean flag = false;

// Thread A
void threadA() {
    value = 1;
    flag = true;
}

// Thread B
void threadB() {
    while (!flag) {
        // Busy-wait
    }
    // What is the value of 'value' here?
    System.out.println("Value is: " + value);
}

If Thread A sets value to 1 and then flag to true, and Thread B spins until flag is true, what value will Thread B see for value? Intuitively, you’d expect 1. But the JVM’s memory model has a trick up its sleeve: the "happens-before" relationship.

The happens-before relationship is a set of rules that defines when a write to memory by one thread becomes visible to another thread. It’s not just about volatile keywords or synchronized blocks; it’s about the underlying guarantees the JVM provides for ordering operations across threads.

Here’s how it works in our example:

  1. Write to value in Thread A: value = 1;
  2. Write to flag in Thread A: flag = true;
  3. Read flag in Thread B: while (!flag)
  4. Read value in Thread B: System.out.println("Value is: " + value);

The crucial rule here is: A write to a volatile variable happens-before any subsequent read of that same volatile variable.

In our example, the write to flag (which is volatile) in Thread A happens-before the read of flag in Thread B. This means that when Thread B sees flag as true, it’s guaranteed that all writes that happened before that flag write in Thread A are now visible to Thread B. Since value = 1 happened before flag = true in Thread A, Thread B is guaranteed to see value as 1.

Now, let’s consider what happens if flag were not volatile.

// Shared mutable state
int value = 0;
boolean flag = false; // Not volatile

// Thread A
void threadA() {
    value = 1;
    flag = true;
}

// Thread B
void threadB() {
    while (!flag) {
        // Busy-wait
    }
    // What is the value of 'value' here?
    System.out.println("Value is: " + value);
}

Without volatile on flag, there’s no guaranteed happens-before relationship between value = 1 and flag = true in Thread A, nor between flag = true and the read of value in Thread B. The JVM is free to reorder these operations for performance. It could, for example, execute flag = true before value = 1. In this scenario, Thread B might see flag as true but value as 0, because the write to value hasn’t become visible yet.

The happens-before rules are transitive. If operation A happens-before B, and B happens-before C, then A happens-before C. This allows us to reason about complex sequences of operations.

Here are the key happens-before relationships to remember:

  • Program-order rule: Actions within a single thread are executed in program order. This is the most basic rule, but the JVM can still reorder instructions as long as it doesn’t violate other happens-before guarantees.
  • Volatile variable rule: A write to a volatile variable v happens-before any subsequent read of v.
  • Lock rule: A thread’s unlocking of a mutex happens-before any subsequent lock of the same mutex by any thread. This ensures that all writes made by the unlocking thread are visible to the thread that subsequently acquires the lock.
  • Thread start rule: A thread’s starting of another thread (t.start()) happens-before any action in the newly started thread.
  • Thread termination rule: Any action in a thread happens-before another thread’s detection of the first thread’s termination (e.g., via Thread.join() or Thread.isAlive()).
  • Thread interruption rule: A thread’s interruption of another thread (t.interrupt()) happens-before the interrupted thread’s detection of the interruption.
  • Object construction rule: The initialization of an object happens-before the object is made available to other threads. This includes constructor completion and the finalize() method.

When you use synchronized blocks, they implicitly establish happens-before relationships. The unlocking of a synchronized block establishes a happens-before relationship with the subsequent locking of the same block. This ensures that all writes performed by the thread exiting the synchronized block are visible to the thread entering it.

The surprising part for many is that even with volatile, the order of writes within the same thread is preserved, but there’s no guarantee that an arbitrary write in one thread will be seen by another thread unless a happens-before relationship connects them. It’s not just about making values visible; it’s about establishing a causal chain of events across threads.

If you forget to make flag volatile in the example, and Thread B sees value as 0, the next error you’ll hit is a NullPointerException when you try to dereference a null object that was supposed to have been initialized by a thread that hadn’t yet made its writes visible.

Want structured learning?

Take the full Jvm course →