The Java Memory Model
The rules that decide when a write by one thread becomes visible to another, expressed as the happens-before relationship.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
The problem it solves
// Thread A
data = 42;
ready = true;
// Thread B
if (ready) {
System.out.println(data); // may print 0
}Both the compiler and the processor may reorder independent instructions, and each processor core has its own caches. Without a rule, thread B could see ready as true while still reading a stale data. The Java Memory Model defines exactly which reorderings are permitted and when a write must be visible.
happens-before
If action A happens-before action B, then everything A did is visible to B. It is not about wall clock time; it is a guarantee about visibility and ordering.
| Rule | Guarantee |
|---|---|
| Program order | Within one thread, each statement happens-before the next |
| Monitor lock | Releasing a lock happens-before another thread acquires it |
| Volatile | A write to a volatile field happens-before every later read of it |
| Thread start | thread.start() happens-before anything in that thread |
| Thread join | Everything in a thread happens-before join() returns |
| Final fields | Fields set in a constructor are visible once it completes safely |
| Transitivity | If A happens-before B and B happens-before C, then A happens-before C |
Fixing the example
private int data;
private volatile boolean ready; // volatile creates the edge
// Thread A
data = 42;
ready = true; // the write to data cannot move after this
// Thread B
if (ready) { // if this reads true...
System.out.println(data); // ...42 is guaranteed to be visible
}A volatile write publishes everything written before it; a volatile read sees all of it. This is why one volatile flag can safely guard several ordinary fields.
Locks give the same guarantee
synchronized (lock) {
data = 42;
}
// release happens-before the next acquire
synchronized (lock) {
System.out.println(data); // 42 is visible
}Note that both threads must use the same lock. Locking different objects creates no ordering between them at all.
Safe publication
// Unsafe: another thread may see a partially constructed object
public class Registry {
private static Registry instance;
public static Registry get() {
if (instance == null) {
instance = new Registry(); // the reference may be visible first
}
return instance;
}
}An object is safely published when it is made visible in a way that guarantees other threads see it fully constructed. The reliable ways are:
- Initialise it in a static initialiser.
- Store it in a
volatilefield or anAtomicReference. - Store it in a
finalfield of a properly constructed object. - Store it in a field guarded by a lock, and read it under the same lock.
- Put it into a concurrent collection, which publishes safely.
// The static holder idiom: the JVM guarantees a class is initialised once
public class Registry {
private static class Holder {
static final Registry INSTANCE = new Registry();
}
public static Registry get() {
return Holder.INSTANCE;
}
}The final field guarantee
public final class Config {
private final Map<String, String> values;
public Config(Map<String, String> values) {
this.values = Map.copyOf(values);
}
public String get(String key) {
return values.get(key);
}
}Provided the constructor does not let this escape before it finishes, every thread that obtains a reference to the object sees its final fields fully initialised, with no synchronisation at all. This is a large part of why immutable objects are the simplest answer to concurrency.
// Leaking this from a constructor breaks the guarantee
public Config(Registry registry) {
registry.register(this); // other threads can see a half built object
this.values = load();
}Atomicity of individual operations
| Operation | Atomic |
|---|---|
| Reading or writing any type up to 32 bits | Yes |
| Reading or writing a reference | Yes |
Reading or writing a non volatile long or double | Not guaranteed |
Reading or writing a volatile long or double | Yes |
count++ | No, it is three operations |
On a 32 bit JVM a long write can be split into two halves, so another thread could observe a value that was never assigned. Declaring it volatile or protecting it with a lock removes the possibility.
Reordering in practice
Source order A possible execution
data = 42; ready = true; compiler and CPU may swap these
ready = true; data = 42; because they are independentWithin a single thread the result is always as if nothing moved. The reordering is only observable from another thread, which is why concurrency bugs of this kind are so difficult to reproduce.
Practical guidance
- Prefer immutability. An object that never changes needs no memory model reasoning.
- Prefer confinement. State touched by one thread has no visibility problem.
- Publish safely when a shared object must be handed over.
- Use the concurrent utilities, which are already correct.
- Use
volatilefor flags, and locks or atomics for anything compound. - Document which lock guards which field.
public class Statistics {
private final Object lock = new Object();
// Guarded by lock
private long total;
private int count;
public void record(long value) {
synchronized (lock) {
total += value;
count++;
}
}
public double average() {
synchronized (lock) {
return count == 0 ? 0 : (double) total / count;
}
}
}Common mistakes
- Assuming a write is visible simply because it happened earlier in time.
- Synchronising on different objects and expecting an ordering between them.
- Publishing an object through a plain field.
- Letting
thisescape from a constructor. - Treating a non volatile
longas atomic. - Testing on one machine and concluding the code is correct; different processors reorder differently.
Practice
- Write the flag and data example without
volatileand explain why it may misbehave. - List three ways to publish an object safely.
- Why does a
finalfield need no synchronisation to be visible? - Explain why locking two different objects gives no happens-before edge.
- Show how leaking
thisfrom a constructor breaks the final field guarantee.
Conclusion
The memory model is a set of happens-before edges created by locks, volatile fields, thread start and join, and final fields. Build your program from immutable objects and safe publication, and most of these rules never have to be applied by hand.