The Java Memory Model (JMM): The Physics of Java
Writing thread-safe Java code requires more than just adding synchronized to every method. To build truly high-performance concurrent systems, you must understand the Java Memory Model (JMM), which defines how threads interact through memory.
1. The Core Problem: CPU Caches and Reordering
Modern CPUs don't read directly from RAM; they use multiple levels of fast L1/L2/L3 caches.
- Visibility: If Thread A updates a variable, Thread B might not see that update immediately because it's still reading the old value from its own CPU cache.
- Reordering: To optimize performance, the Compiler and CPU might change the order of your instructions as long as the result of a single thread remains the same. In multithreaded code, this can be disastrous.
2. The volatile Keyword
The volatile keyword is the simplest way to ensure visibility and prevent reordering.
- Visibility: Any write to a
volatilevariable is immediately flushed to main memory. Any read is taken directly from main memory. - No Reordering:
volatileprevents the compiler from moving code around the variable access (Memory Barriers). - Limitation:
volatiledoes not provide atomicity.count++on a volatile variable is still not thread-safe.
3. The 'Happens-Before' Relationship
The JMM defines a set of rules called "happens-before." If action A happens-before action B, the results of A are guaranteed to be visible to B.
- Locking: Releasing a lock happens-before acquiring the same lock.
- Volatile: A write to a volatile variable happens-before every subsequent read of that variable.
- Thread Start: Calling
thread.start()happens-before any action in that thread. - Transitivity: If A happens-before B, and B happens-before C, then A happens-before C.
4. Final Fields and Safety
The JMM provides special guarantees for final fields. If an object is "properly constructed" (the this reference doesn't escape during the constructor), then other threads are guaranteed to see the correct values of final fields without any synchronization.
5. Practical Implementation
When should you use what?
- synchronized / ReentrantLock: Use when you need both visibility and atomicity.
- volatile: Use for "state flags" (e.g.,
boolean stopRequested) where you only need visibility. - AtomicInteger / AtomicReference: Use for single-variable updates where you need atomicity without the overhead of locks.
Summary
The JMM is the contract between the Java developer and the hardware. By understanding visibility and the happens-before relationship, you can write concurrent code that is not only correct but also highly optimized for modern multi-core processors.
