Java

Happens-Before in Java Explained with Practical Examples

5 min read Updated Mar 21, 2026

Java Concurrency Series - Module 1

If you remember only one formal concurrency concept from the Java Memory Model, remember happens-before.

It is the rule that turns “I think another thread should see this” into “the model guarantees another thread can see this.”


Problem Statement

Suppose thread A:

  • updates shared state
  • then sets a signal

Thread B sees the signal. Can it safely assume the state update is also visible?

Without a happens-before edge, not necessarily.

That is the whole point.


Naive Mental Model

The naive model is:

  • if A did it earlier in source code, B sees it later

That is not a safe concurrency rule.

Happens-before is stronger. It says:

  • if action A happens-before action B, then B is guaranteed to observe the effects of A in the required visibility and ordering sense

This is the practical bridge between synchronization and reasoning.


Why Happens-Before Matters

You rarely debug concurrency at the level of CPU caches directly. You debug it by asking:

  • what synchronization exists?
  • what does that synchronization guarantee?
  • is there a happens-before edge between the writer and the reader?

If the answer is no, your code is relying on timing.


Common Happens-Before Sources

The most important ones for application code are:

  • monitor unlock -> later lock on the same monitor
  • volatile write -> later volatile read of the same variable
  • actions before Thread.start() -> actions inside the started thread
  • actions in a thread before completion -> actions after another thread successfully join()s it

These are the edges you will use repeatedly in real Java code.


Runnable Example: Volatile Signal

public class HappensBeforeVolatileDemo {

    private static int data;
    private static volatile boolean ready;

    public static void main(String[] args) throws Exception {
        Thread writer = new Thread(() -> {
            data = 42;
            ready = true;
        }, "writer");

        Thread reader = new Thread(() -> {
            while (!ready) {
                // wait
            }
            System.out.println("Observed data = " + data);
        }, "reader");

        reader.start();
        writer.start();

        writer.join();
        reader.join();
    }
}

Why this works:

  • the write to ready is volatile
  • the later read of ready is volatile
  • that establishes a happens-before edge
  • once the reader sees ready == true, it can also safely observe the earlier write to data

This is one of the most important visibility patterns in Java.


Runnable Example: join as a Visibility Boundary

public class HappensBeforeJoinDemo {

    private static int result;

    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(() -> result = 99, "worker");

        worker.start();
        worker.join();

        System.out.println("Result after join = " + result);
    }
}

Why this works:

  • the worker’s actions before completion happen-before the main thread continues after join

That means join is not only waiting. It is also a visibility boundary.


Broken Shape Without a Real Edge

public class BrokenSignalDemo {

    private static int data;
    private static boolean ready;

    public static void main(String[] args) throws Exception {
        Thread writer = new Thread(() -> {
            data = 42;
            ready = true;
        });

        Thread reader = new Thread(() -> {
            while (!ready) {
                // spin
            }
            System.out.println(data);
        });

        reader.start();
        writer.start();
    }
}

Why this is unsafe:

  • there is no guaranteed happens-before edge between the writer’s plain writes and the reader’s plain reads

It may work. That is not the same as being correct.


Production-Style Example

Imagine a cache refresher thread:

  1. loads latest pricing rules
  2. validates them
  3. publishes the new snapshot

Request threads then:

  1. read the published reference
  2. make pricing decisions

The key design question is: what publication step creates the happens-before edge?

Good answers:

  • volatile reference write
  • atomic reference set
  • publishing under a lock and reading under the same lock

Bad answer:

  • “the refresher runs first most of the time”

That is timing, not correctness.


Performance and Trade-Offs

Happens-before is not a separate API. It is a property created by correct synchronization.

That means performance questions become design questions:

  • do you need a lock because multiple variables must move together?
  • is a volatile signal enough because only visibility matters?
  • can immutable snapshots reduce the synchronization surface?

Good concurrency design is often just good happens-before design.


Testing and Debugging Notes

In code reviews, try asking:

  1. where is the writer?
  2. where is the reader?
  3. what exact happens-before edge connects them?

If nobody can answer, the code is probably relying on luck.

This question is so useful that it should become a habit.


Decision Guide

  • use volatile when you need a simple visibility edge
  • use locks when several operations must become visible as one protected critical section
  • use join and task completion boundaries intentionally, not accidentally

The core idea stays the same: concurrent correctness needs an explicit edge.


Key Takeaways

  • happens-before is the practical visibility and ordering rule in Java concurrency
  • without it, shared-state reasoning is unreliable
  • volatile, locks, start, and join all matter because they create happens-before edges

Next Post

Atomicity Visibility and Ordering in Java Concurrency

Categories

Tags

Continue reading

Previous Introduction to the Java Memory Model for Backend Engineers Next Atomicity Visibility and Ordering in Java Concurrency

Comments