Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 38

Understanding Happens-Before: Constraints for Predictable Outcomes

Recommended resources

Introduction

In our last lesson, we confronted the chaos that can arise from instruction reordering. We saw how compilers and CPUs, in their quest for performance, can rearrange operations in ways that are invisible to a single thread but can catastrophically break multithreaded code. You saw how this could lead to a reader thread seeing a true flag but still reading stale data, a phenomenon that defies sequential reasoning.

Today, we move from the problem to the solution. The Java Memory Model (JMM) provides a formal framework to bring order to this potential chaos: the happens-before relationship. This is not a description of how code actually runs on hardware in real-time, but rather a set of rules that act as a contract. If you, the programmer, abide by these rules, the JMM guarantees that the effects of reordering will not break your program's logic. This lesson will define the happens-before relationship and walk through its key rules, showing you how they constrain reordering to ensure predictable outcomes in your concurrent programs.


What is the Happens-Before Relationship?

At its core, the happens-before relationship is a guarantee of memory visibility and ordering between operations. If we say that action X happens-before action Y, the JMM guarantees two things:

  1. All memory writes and other effects of action X are visible to the thread performing action Y.
  2. Action X is guaranteed to finish before action Y begins.

This relationship creates a partial ordering on all the operations in your program. It doesn't order every single action relative to every other action—that would kill performance. Instead, it only imposes ordering where you explicitly ask for it using specific language constructs. This allows optimizers maximum freedom to reorder operations that are not bound by a happens-before relationship.

The official JSR 133 FAQ provides the foundational definition.

JSR 133 (Java Memory Model) FAQ

This document, written by the experts who designed the Java Memory Model, is the definitive source on this topic.

Please read the introduction to the happens-before relationship. Start at the paragraph "The new memory model semantics..." and continue through the first bullet point. This section introduces the concept and its primary consequence: guaranteeing visibility.

Now, let's explore the specific rules that establish this crucial relationship.


The Key Rules of Happens-Before

The JMM defines several rules that create happens-before edges between operations. Understanding these is the key to writing correct concurrent code.

1. Program Order Rule

Each action in a thread happens-before every action in that thread that comes later in the program's order.

This is the most intuitive rule. Within a single thread, if you write x = 5; on one line and y = x; on the next, you are guaranteed that y will be assigned the value 5. This rule preserves the "as-if-serial" semantics we discussed in the last lesson. While the compiler or CPU might reorder instructions internally, the final observable result within that single thread must be consistent with the code you wrote.

2. Monitor Lock Rule

An unlock on a monitor happens-before every subsequent lock on that same monitor.

This is the rule that gives synchronized blocks their memory visibility power, beyond just mutual exclusion.

  • When a thread exits a synchronized block, it performs a release on the monitor. This flushes all of its local cache changes to main memory.
  • When a thread enters a synchronized block, it performs an acquire on the monitor. This invalidates its local cache, forcing it to reload variables from main memory.

Because the release happens-before the acquire, any variable written by Thread A before or inside a synchronized block is guaranteed to be visible to Thread B after it enters a synchronized block on the same object.

The following video segment explains how this rule prevents reordering around synchronized blocks.

Java Happens Before Guarantee - Java Memory Model - Part 2

Jakob Jenkov provides a clear, animated view of how synchronized blocks constrain reordering.

Watch the section from this explanation of the happens-before guarantee for synchronized blocks. Focus on how writes before the exit cannot be moved after the exit, and reads after the entrance cannot be moved before the entrance. This is the practical enforcement of the Monitor Lock Rule.

3. Volatile Variable Rule

A write to a volatile field happens-before every subsequent read of that same volatile field.

This rule formally defines the power of the volatile keyword. A write to a volatile variable has the same memory effect as a monitor release, and a read has the same effect as a monitor acquire.

This is what fixes the reordering problem from our previous lesson. Let's revisit that example:

class ProducerConsumer {
  int number = 0;
  volatile boolean ready = false; // The key change!

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

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

Because ready is volatile, the write to it (Action B) happens-before the read of it (Action C). Crucially, the Program Order Rule states that the write to number (Action A) happens-before the write to ready (Action B).

By transitivity (which we'll cover next), this means the write to number (A) happens-before the read of ready (C). Therefore, if the reader thread sees ready as true, it is guaranteed to see the write to number as 42. Reordering number = 42 to occur after ready = true is now forbidden.

The JSR 133 FAQ provides an excellent summary of this.

JSR 133 (Java Memory Model) FAQ

Let's solidify our understanding of volatile's role in the happens-before model.

Read the section titled "What does volatile do?". Pay close attention to the VolatileExample code, as it perfectly mirrors the logic we just discussed. The text explicitly states that writing to a volatile has the same memory effect as a monitor release, and reading has the effect of a monitor acquire.

4. Thread Start and Join Rules

The JMM also provides common-sense guarantees for thread lifecycle operations.

A call to start() on a thread happens-before any actions in the started thread.

This ensures that when a new thread begins execution, it sees all the variables set by the parent thread up to the point start() was called.

All actions in a thread happen-before any other thread successfully returns from a join() on that thread.

This ensures that when a parent thread calls join() on a child thread, upon the child's completion, the parent thread sees all the work done by the child.

The DZone article "Happens-Before In Java" has useful diagrams illustrating these two rules.

Happens-Before In Java, Write a Thread-Safe App

This article provides visual aids for the thread lifecycle rules.

In this article, find the section titled "Install Happens-Before Relationship: Thread Start and Join". Review the two code examples and their accompanying diagrams. The first shows how data is passed to a new thread via start(), and the second shows how results are retrieved from a completed thread via join().


Transitivity: The Chain of Guarantees

Perhaps the most powerful aspect of the happens-before relationship is that it is transitive. If A happens-before B, and B happens-before C, then A happens-before C. This allows us to chain together the rules to create robust visibility guarantees.

Let's look at the volatile example again through this lens.

  1. number = 42; hb ready = true; (Program Order Rule in writer thread)
  2. ready = true; hb if (ready) (Volatile Variable Rule across threads)
  3. Therefore, by transitivity: number = 42; hb if (ready)

This chain of guarantees is what ensures the reader sees 42. The following diagram perfectly illustrates this composition of rules.

This diagram shows how the Program Order Rule (Action A happens-before B) and the Volatile Variable Rule (Action B happens-before C) are combined by the Transitivity Rule to guarantee that Action A happens-before C. This ensures the reader thread sees the correct value.

The Big Picture: Your Contract with the JVM

The happens-before rules are the bridge between the developer's need for a predictable, sequential world and the hardware's reality of parallel, reordered execution.

This diagram conceptualizes the role of Happens-Before Rules. They act as a mediating layer, allowing the compiler and processor to perform safe optimizations (reordering) while still providing the developer with the guarantees needed for a predictable, strong memory model.

When you write code that has a data race—a write to a variable by one thread and a read of that same variable by another thread, where the two are not ordered by a happens-before relationship—you are breaking the contract. In this case, the JMM makes no guarantees about visibility. The result is undefined and your program is incorrect.

By using volatile, synchronized, java.util.concurrent locks, or thread lifecycle methods, you establish happens-before relationships. You tell the JVM, "These specific operations are critical for inter-thread communication; do not reorder them in a way that breaks visibility." The JVM, in turn, honors this contract by inserting the necessary memory barriers to constrain reordering and ensure your data is correctly published and consumed across threads.


Conclusion

In this lesson, we demystified the Java Memory Model's core contract: the happens-before relationship. We've moved beyond the fear of reordering and into the structured world of guarantees that the JMM provides.

Key Takeaways:

  • Happens-before is a contract: It guarantees memory visibility and ordering between specific actions if you use the right constructs.
  • It creates a partial ordering: This allows for performance optimizations (reordering) where no explicit happens-before edge exists.
  • Key Rules: The rules for Program Order, Monitor Locks, Volatile Variables, and Thread Start/Join are the building blocks for creating these guarantees.
  • Transitivity is key: The rules can be chained together to build complex and reliable communication patterns between threads.
  • Avoid data races: If a write and a read between threads are not ordered by a happens-before relationship, your program has a data race and its behavior is undefined.

In our previous lessons, we've discussed memory visibility issues stemming from reordering and CPU caches. While this lesson focused on the software model (happens-before) that constrains reordering, our next lesson will briefly touch on the hardware reality of CPU cache coherence protocols, giving you a high-level appreciation for how modern processors work to make these software guarantees physically possible.