Chapter 16: The Java Memory Model
Multithreaded programming in Java requires more than just using synchronized
or volatile
. To reason correctly about visibility, ordering, and atomicity between threads, you need to understand the Java Memory Model (JMM). Chapter 16 of Java Concurrency in Practice dives into this essential topic. In this post, we’ll break it down by examining what really happens under the hood with a deceptively simple line of code:
aVariable = 3;
What Really Happens Under the Hood
On the surface, this looks like a direct write to memory. But on modern systems with CPU caches, compiler optimizations, and multiple threads, it involves:
- Compiler reordering (for optimization)
- CPU instruction reordering and store buffers
- Cache coherence between CPU cores
- Memory barriers or fences (explicit or implied)
- Delayed visibility to other threads
In the absence of synchronization, a thread may see a stale or inconsistent value of aVariable
. The Java Memory Model was designed to handle these complexities in a way that gives developers rules to reason about concurrency across platforms.
Key Concepts of the Java Memory Model
- Visibility: Whether one thread sees the results of another thread’s changes
- Ordering: The order in which reads and writes happen with respect to program and hardware
- Atomicity: Whether operations are indivisible (safe from race conditions)
- Happens-Before: A key relationship that ensures visibility and ordering between operations
Happens-Before Examples
- Thread A writing to a
volatile
variable happens-before Thread B reading it - Unlocking a synchronized block happens-before another thread locks the same monitor
- Starting a thread happens-before its
run()
method
Safe Initialization Patterns
1. Thread-Safe Lazy Initialization (Broken Version)
private SomeObject instance;
public SomeObject getInstance() {
if (instance == null) {
instance = new SomeObject(); // not thread-safe!
}
return instance;
}
The above version is not thread-safe. Without proper synchronization, one thread could see a partially constructed object or a stale null
value.
2. Eager Initialization (Always Safe)
private final SomeObject instance = new SomeObject();
public SomeObject getInstance() {
return instance;
}
This is always safe because final
fields initialized during object construction are guaranteed to be visible correctly to all threads due to the JMM's initialization safety.
3. Synchronized Lazy Initialization
private SomeObject instance;
public synchronized SomeObject getInstance() {
if (instance == null) {
instance = new SomeObject();
}
return instance;
}
This approach is thread-safe, but synchronized
adds overhead even after the instance is initialized.
4. Double-Checked Locking with Volatile
private volatile SomeObject instance;
public SomeObject getInstance() {
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = new SomeObject();
}
}
}
return instance;
}
This idiom is safe starting from Java 5 and onward due to volatile
guaranteeing visibility and preventing instruction reordering.
5. Initialization-on-Demand Holder Class Idiom (Best Practice)
public class Holder {
private static class LazyHolder {
static final SomeObject INSTANCE = new SomeObject();
}
public static SomeObject getInstance() {
return LazyHolder.INSTANCE;
}
}
This idiom is both thread-safe and highly performant. The class loader guarantees that INSTANCE
is created only when accessed and ensures proper synchronization under the JMM.
Initialization Safety for Immutable Objects
Immutable objects, such as those using final
fields, have special support from the Java Memory Model. When an object is safely published (e.g., stored in a volatile
field or inside a synchronized block), all threads will see the initialized state of its final
fields correctly.
public class ImmutableRGB {
private final int red;
private final int green;
private final int blue;
public ImmutableRGB(int r, int g, int b) {
this.red = r;
this.green = g;
this.blue = b;
}
public int getRGB() {
return ((red << 16) | (green << 8) | blue);
}
}
Once an instance of ImmutableRGB
is constructed and published, all threads will observe its fully initialized state—even without synchronization.
Conclusion
The Java Memory Model provides the foundation for writing safe and performant concurrent code. Understanding how operations like aVariable = 3
translate through CPU caches, compilers, and memory barriers will help you reason more clearly about visibility and correctness. Using volatile
, synchronized
, or atomic classes from java.util.concurrent
ensures your concurrent objects behave reliably across all platforms.
No comments:
Post a Comment