Fundamentals 28 min read

Understanding the Happens‑Before Principle in Java Concurrency

This article explains the Java Happens‑Before principle, detailing its rules such as program order, monitor lock, volatile variable, thread start, termination, interruption, and finalizer, and demonstrates each rule with clear code examples to illustrate visibility guarantees in concurrent programming.

Zhuanzhuan Tech
Zhuanzhuan Tech
Zhuanzhuan Tech
Understanding the Happens‑Before Principle in Java Concurrency

Definition of the Happens‑Before Principle

The Happens‑Before (HB) principle is a higher‑level, language‑level description of visibility in the Java Memory Model (JMM). It allows developers to reason about the visibility of operations in a concurrent environment without delving into the low‑level JMM specifications.

Program Order Rule : Within a single thread, actions that appear earlier in the control‑flow happen‑before actions that appear later.

Monitor Lock Rule : An unlock operation happens‑before a subsequent lock on the same monitor.

Volatile Variable Rule : A write to a volatile variable happens‑before any later read of that same variable.

Thread Start Rule : The call to Thread.start() happens‑before every action in the newly started thread.

Thread Termination Rule : All actions in a thread happen‑before another thread detects its termination (e.g., via Thread.join() or Thread.isAlive() ).

Thread Interruption Rule : A call to Thread.interrupt() happens‑before the target thread detects the interrupt (e.g., via Thread.interrupted() ).

Finalizer Rule : The end of an object's constructor happens‑before the start of its finalize() method.

Transitivity : If A happens‑before B and B happens‑before C, then A happens‑before C.

Interpretation of “happens‑before”

The phrase does not describe an active ordering constraint; it describes an objective result observed at runtime. For example, when an unlock happens‑before a lock on the same monitor, the effects of the unlock (including any writes performed before it) become visible to the thread that subsequently acquires the lock.

Code Examples

1. Monitor Lock Rule

1.1 WithoutMonitorLockRule

This example starts two threads, updater and getter . The updater sets stop = true without any synchronization, so the getter never sees the change.

public class WithoutMonitorLockRule {
    private static boolean stop = false;
    public static void main(String[] args) {
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                Utils.sleep(1000);
                stop = true;
                System.out.println("updater set stop true.");
            }
        }, "updater");
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (stop) {
                        System.out.println("getter stopped");
                        break;
                    }
                }
            }
        }, "getter");
        getter.start();
        updater.start();
    }
}

1.2 MonitorLockRuleSynchronized

Using a synchronized block on a shared lock object makes the write to stop visible to the getter.

public class MonitorLockRuleSynchronized {
    private static boolean stop = false;
    private static final Object lockObject = new Object();
    public static void main(String[] args) {
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                Utils.sleep(1000);
                synchronized (lockObject) {
                    stop = true;
                    System.out.println("updater set stop true.");
                }
            }
        }, "updater");
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (lockObject) {
                        if (stop) {
                            System.out.println("getter stopped");
                            break;
                        }
                    }
                }
            }
        }, "getter");
        getter.start();
        updater.start();
    }
}

1.3 MonitorLockRuleReentrantLock

The same guarantee can be achieved with java.util.concurrent.locks.ReentrantLock , which is required to provide the same memory‑synchronization semantics as the built‑in monitor.

public class MonitorLockRuleReentrantLock {
    private static boolean stop = false;
    private static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) {
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                Utils.sleep(1000);
                reentrantLock.lock();
                try {
                    stop = true;
                    System.out.println("updater set stop true.");
                } finally {
                    reentrantLock.unlock();
                }
            }
        }, "updater");
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    reentrantLock.lock();
                    try {
                        if (stop) {
                            System.out.println("getter stopped");
                            break;
                        }
                    } finally {
                        reentrantLock.unlock();
                    }
                }
            }
        }, "getter");
        getter.start();
        updater.start();
    }
}

2. Volatile Variable Rule

2.1 WithoutVolatileRule

When stop is not declared volatile , the write performed by the updater thread is not guaranteed to be visible to the getter.

public class WithoutVolatileRule {
    private static boolean stop = false;
    public static void main(String[] args) {
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                Utils.sleep(1000);
                stop = true;
                System.out.println("updater set stop true");
            }
        }, "updater");
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (stop) {
                        System.out.println("getter stopped");
                        break;
                    }
                }
            }
        }, "getter");
        getter.start();
        updater.start();
    }
}

2.2 VolatileRule

Declaring stop as volatile establishes a HB relationship between the write and any subsequent read, making the change visible.

public class VolatileRule {
    private static volatile boolean stop = false;
    public static void main(String[] args) {
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                Utils.sleep(1000);
                stop = true;
                System.out.println("updater set stop true.");
            }
        }, "updater");
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (stop) {
                        System.out.println("getter stopped.");
                        break;
                    }
                }
            }
        }, "getter");
        getter.start();
        updater.start();
    }
}

2.3 VolatileRule1 (Indirect Visibility)

Even without declaring stop volatile, visibility can be achieved indirectly by writing to a volatile object before reading stop . The HB chain is: write to stop → write to volatileObject → read of volatileObject → read of stop .

public class VolatileRule1 {
    private static boolean stop = false;
    private static volatile Object volatileObject = new Object();
    public static void main(String[] args) {
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                Utils.sleep(1000);
                stop = true;
                volatileObject = new Object();
                System.out.println("updater set stop true.");
            }
        }, "updater");
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    Object vo = VolatileRule1.volatileObject; // volatile read
                    if (stop) {
                        System.out.println("getter stopped.");
                        break;
                    }
                }
            }
        }, "getter");
        updater.start();
        getter.start();
    }
}

3. Thread Start Rule

3.1 WithoutThreadStartRule

The updater thread starts the getter after setting stop . Because the start occurs after the write, the getter does not see the change.

public class WithoutThreadStartRule {
    private static boolean stop = false;
    public static void main(String[] args) {
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (stop) {
                        System.out.println("getter stopped.");
                        break;
                    }
                }
            }
        }, "getter");
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                getter.start();
                Utils.sleep(1000);
                stop = true;
                System.out.println("updater set stop true.");
                while (true) {}
            }
        });
        updater.start();
    }
}

3.2 ThreadStartRule (Correct Ordering)

When the write to stop happens before the call to getter.start() , the HB chain guarantees visibility.

public class ThreadStartRule {
    private static boolean stop = false;
    public static void main(String[] args) {
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (stop) {
                        System.out.println("getter stopped.");
                        break;
                    }
                }
            }
        }, "getter");
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                stop = true;
                System.out.println("updater set stop true.");
                Utils.sleep(1000);
                getter.start();
                while (true) {}
            }
        });
        updater.start();
    }
}

4. Thread Termination Rule

4.1 WithoutThreadTerminationRule

Without joining the updater, the getter may not see the write.

public class WithoutThreadTerminationRule {
    private static boolean stop = false;
    public static void main(String[] args) throws InterruptedException {
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                Utils.sleep(1000);
                stop = true;
                System.out.println("updater set stop true.");
            }
        });
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (stop) {
                        System.out.println("getter stopped.");
                        break;
                    }
                }
            }
        }, "getter");
        updater.start();
        getter.start();
    }
}

4.2 ThreadTerminationRule (Using join)

Calling updater.join() creates a HB edge from all actions in the updater to the point after the join, making the write visible.

public class ThreadTerminationRule {
    private static boolean stop = false;
    public static void main(String[] args) throws InterruptedException {
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                Utils.sleep(1000);
                stop = true;
                System.out.println("updater set stop true.");
            }
        });
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    updater.join();
                } catch (InterruptedException e) {}
                while (true) {
                    if (stop) {
                        System.out.println("getter stopped.");
                        break;
                    }
                }
            }
        }, "getter");
        updater.start();
        getter.start();
    }
}

5. Thread Interruption Rule

5.1 WithoutThreadInterruptRule

No interrupt is issued, so the getter never sees the change.

public class WithoutThreadInterruptRule {
    private static boolean stop = false;
    public static void main(String[] args) {
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (stop) {
                        System.out.println("getter stopped.");
                        break;
                    }
                }
            }
        }, "getter");
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                Utils.sleep(1000);
                stop = true;
                System.out.println("updater set stop true.");
            }
        }, "updater");
        updater.start();
        getter.start();
    }
}

5.2 ThreadInterruptRule

The updater calls getter.interrupt() after setting stop . The interrupt creates a HB edge that makes the write visible to the getter when it checks Thread.interrupted() .

public class ThreadInterruptRule {
    private static boolean stop = false;
    public static void main(String[] args) {
        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        if (stop) {
                            System.out.println("getter stopped.");
                            break;
                        }
                    }
                }
            }
        }, "getter");
        Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                Utils.sleep(1000);
                stop = true;
                getter.interrupt();
                System.out.println("updater set stop true.");
            }
        }, "updater");
        updater.start();
        getter.start();
    }
}

6. Finalizer Rule

6.1 FinalizerRule

The constructor sets stop = true . After the object becomes unreachable, its finalize() method runs in the finalizer thread and observes the value set in the constructor, demonstrating the HB relationship between constructor completion and finalizer start.

public class FinalizerRule {
    private static boolean stop = false;
    public static void main(String[] args) {
        Test test = new Test();
        test = null; // eligible for GC
        while (true) {
            byte[] bytes = new byte[1024 * 1024]; // trigger GC
        }
    }
    static class Test {
        public Test() {
            stop = true;
            System.out.println("set stop true in constructor");
        }
        @Override
        protected void finalize() throws Throwable {
            if (stop) {
                System.out.println("stop true in finalize, threadName " + Thread.currentThread().getName());
            } else {
                System.out.println("stop false in finalize, threadName " + Thread.currentThread().getName());
            }
        }
    }
}

Further Interpretation

The article repeatedly shows that most HB guarantees are derived from the combination of the program‑order rule and transitivity. For example, the monitor‑lock rule can be restated as: the unlock (including all prior actions) happens‑before the subsequent lock, therefore the effects of the unlock become visible to the code protected by the lock.

Similarly, the volatile‑variable rule, thread‑start rule, termination rule, interruption rule, and finalizer rule can all be expressed as a base ordering followed by transitive propagation of visibility.

Conclusion

The Happens‑Before principle is essential for writing correct and efficient multithreaded Java code. By abstracting the low‑level JMM details into a set of intuitive ordering rules and providing concrete code demonstrations, this article aims to make the principle accessible to developers and help them avoid subtle visibility bugs.

JavaConcurrencyThread SafetyMemory ModelHappens-Before
Zhuanzhuan Tech
Written by

Zhuanzhuan Tech

A platform for Zhuanzhuan R&D and industry peers to learn and exchange technology, regularly sharing frontline experience and cutting‑edge topics. We welcome practical discussions and sharing; contact waterystone with any questions.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.