Class 44: Java Memory Model and Happens-Before
Part 8 — Reliability, Debugging and Real-World Applications
Welcome back!
In our previous classes, we learned how multiple threads can share data and how to protect that data using synchronization, volatile, atomic variables, and locks.
But have you ever wondered how Java ensures that changes made by one thread become visible to another?
Let's understand this with a simple example.
1. The Visibility Problem
Imagine that we have two threads.
- Thread 1 updates a shared variable.
- Thread 2 reads that shared variable.
We might expect Thread 2 to immediately see the latest value written by Thread 1.
However, concurrent programs are more complicated than that.
Java allows the JVM, compiler, and hardware to optimize how operations are performed.
Because of these optimizations, without proper synchronization, one thread is not guaranteed to observe another thread's latest update.
This is called a visibility problem.
Let's look at an example.
public class VisibilityProblem {
private static boolean running = true;
public static void main(String[] args)
throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) {
// Keep working while running is true
}
System.out.println("Worker stopped");
});
worker.start();
Thread.sleep(1000);
running = false;
System.out.println("Main thread updated running");
}
}
The main thread changes running to false.
But the worker thread is not guaranteed to observe that update.
The worker might continue executing its loop because there is no synchronization mechanism establishing visibility between these operations.
This example contains a data race.
We should not depend on the worker eventually seeing the updated value.
2. What Is the Java Memory Model?
The Java Memory Model (JMM) defines the rules that determine how threads interact through memory.
In simple words, it helps answer questions such as:
- When does one thread see changes made by another thread?
- Which operations are guaranteed to happen before others?
- What ordering guarantees do synchronization mechanisms provide?
The Java Memory Model does not simply say that every thread must read every variable directly from main memory every time.
Instead, it defines the guarantees that a correct multithreaded program can rely on.
These guarantees help us write code that behaves correctly even when the JVM and hardware perform optimizations.
3. Understanding Happens-Before
One of the most important concepts in the Java Memory Model is the happens-before relationship.
Let's understand it with a simple situation.
Imagine Thread 1 performs two operations:
int number = 10;
System.out.println(number);
Within the same thread, the assignment happens before the print statement.
The happens-before relationship gives us a guarantee about the ordering and visibility of operations.
More importantly, it can establish guarantees between different threads.
If operation A happens-before operation B, the effects of A are guaranteed to be visible to B, according to the Java Memory Model.
Remember:
Happens-before is about guaranteed ordering and visibility, not necessarily about which thread physically executes first in real time.
4. Happens-Before Through synchronized
We have already learned about synchronized methods and blocks.
Synchronization provides an important happens-before guarantee.
When one thread releases a monitor lock, that release happens-before another thread subsequently acquires the same monitor lock.
Let's understand this with an example.
public class SynchronizedVisibility {
private int number = 0;
public synchronized void update() {
number = 100;
}
public synchronized void display() {
System.out.println(number);
}
public static void main(String[] args) {
SynchronizedVisibility obj =
new SynchronizedVisibility();
Thread thread1 = new Thread(() -> {
obj.update();
});
Thread thread2 = new Thread(() -> {
obj.display();
});
thread1.start();
thread1.join();
thread2.start();
}
}
Here is what happens:
- Thread 1 enters the synchronized update() method.
- It changes number to 100.
- Thread 1 exits the method and releases the object's monitor lock.
- Thread 2 enters the synchronized display() method and acquires the same lock.
- Thread 2 reads number.
The release of the monitor lock by Thread 1 happens before Thread 2 subsequently acquires that same lock.
Therefore, Thread 2 is guaranteed to see the update made by Thread 1.
In this particular example, join() also establishes an ordering guarantee: Thread 1's actions happen-before the main thread returns from thread1.join().
Since Thread 2 is started after that join returns, Thread 1's actions are also ordered before Thread 2's actions.
We will learn more about thread-related happens-before rules shortly.
5. Happens-Before Through volatile
We learned about the volatile keyword in Class 17.
A volatile variable provides visibility and ordering guarantees between threads.
A write to a volatile variable happens-before every subsequent read of that same variable.
Let's revisit our earlier example.
public class VolatileVisibility {
private static volatile boolean running = true;
public static void main(String[] args)
throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) {
// Keep working while running is true
}
System.out.println("Worker stopped");
});
worker.start();
Thread.sleep(1000);
running = false;
System.out.println("Main thread updated running");
}
}
What changed?
We added the volatile keyword to the running variable.
Now, when the main thread writes false to running, a subsequent read of running by the worker thread is guaranteed to observe that write, assuming no later write changes the value.
The worker can therefore exit its loop.
Remember that volatile provides visibility and ordering guarantees, but it does not make compound operations such as count++ atomic.
6. Happens-Before Through Thread.start()
Another important rule involves starting a thread.
All actions performed by a thread before calling start() happen-before the actions performed by the newly started thread.
Let's understand this with an example.
public class ThreadStartExample {
private static int number = 0;
public static void main(String[] args) {
number = 100;
Thread worker = new Thread(() -> {
System.out.println(number);
});
worker.start();
}
}
The main thread updates number before starting the worker thread.
Because of the happens-before guarantee provided by start(), the worker thread is guaranteed to see the value 100.
7. Happens-Before Through Thread.join()
We have used join() several times throughout this course.
Remember, join() makes one thread wait until another thread completes.
It also provides an important visibility guarantee.
All actions performed by a thread happen-before another thread successfully returns from joining it.
Let's look at an example.
public class ThreadJoinExample {
private static int result = 0;
public static void main(String[] args)
throws InterruptedException {
Thread worker = new Thread(() -> {
result = 500;
});
worker.start();
worker.join();
System.out.println(result);
}
}
Here is what happens:
- The worker thread starts.
- The worker updates result to 500.
- The main thread calls join() and waits.
- The worker completes.
- The main thread returns from join().
- The main thread reads result.
The worker's update happens-before the main thread returns from join().
Therefore, the main thread is guaranteed to see result as 500.
8. A Simple List of Important Happens-Before Rules
Let's bring together the rules we have learned.
| Operation A | Operation B | Guarantee |
|---|---|---|
| Earlier action in a thread | Later action in the same thread | Program order |
| Actions before Thread.start() | Actions in the started thread | Start guarantee |
| Thread actions | Another thread returns from join() | Join guarantee |
| Release of a monitor lock | Subsequent acquisition of the same lock | Synchronization guarantee |
| Write to a volatile variable | Subsequent read of the same variable | Volatile guarantee |
These are some of the most useful happens-before relationships to understand when writing Java multithreaded applications.
9. What About Atomic Variables?
We also learned about AtomicInteger in Class 18.
Atomic variables provide thread-safe operations such as incrementAndGet().
They use atomic and memory-ordering mechanisms provided by Java to ensure their documented concurrency guarantees.
For example:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private static AtomicInteger count =
new AtomicInteger(0);
public static void main(String[] args) {
count.incrementAndGet();
System.out.println(count.get());
}
}
The increment operation is atomic.
Atomic variables are useful when we need to perform specific thread-safe operations without writing our own synchronized block.
10. Why Is the Java Memory Model Important?
Let's imagine that we are building a real-world application where multiple threads process customer orders.
One thread updates an order's status, while another thread checks whether the order is ready for dispatch.
If the second thread cannot reliably see the update, the application may make an incorrect decision.
This is why understanding visibility and ordering is important.
The Java Memory Model helps us understand which guarantees we receive when using synchronization, volatile, thread lifecycle methods, and concurrency utilities.
We do not need to memorize every technical detail of the Java Memory Model today.
The important thing is to understand that threads need proper synchronization and communication mechanisms to share information safely.
11. Quick Revision
- The Java Memory Model defines how threads interact through memory.
- Visibility means one thread can observe changes made by another thread.
- Happens-before provides ordering and visibility guarantees.
- Actions before Thread.start() happen-before actions in the started thread.
- Actions in a thread happen-before another thread successfully returns from joining it.
- Releasing a monitor lock happens-before another thread subsequently acquires the same lock.
- A volatile write happens-before a subsequent read of the same volatile variable.
- Atomic variables provide thread-safe operations with defined memory guarantees.
Test Your Understanding
Let's see how well you understood this class!
Question 1: What does the Java Memory Model define?
Question 2: What does happens-before provide?
Question 3: What happens when one thread releases a synchronized monitor lock?
Question 4: What is the happens-before guarantee provided by volatile?
Question 5: What guarantee does Thread.join() provide?
Class Summary
Let's revise today's important concepts.
- The Java Memory Model defines how threads interact through memory.
- Visibility ensures that changes made by one thread can be observed by another when the required guarantees are established.
- Happens-before defines ordering and visibility relationships between operations.
- Thread.start() establishes a happens-before relationship between actions before the call and actions in the started thread.
- Thread.join() establishes a happens-before relationship between the completed thread's actions and the successful return from join().
- synchronized provides visibility through monitor lock release and subsequent acquisition.
- volatile provides visibility and ordering guarantees for reads and writes of the same variable.
- Atomic variables provide thread-safe operations with defined concurrency guarantees.
Remember: When working with multiple threads, never assume that a change made by one thread will automatically become visible to another. Use the appropriate Java concurrency mechanisms to establish the guarantees your application needs.
You have completed 44 out of 46 classes.