JA
Memory Model
Java advanced v1.0.0
Java Memory Model
Overview
The Java Memory Model (JMM) defines how threads interact through memory and what behaviors are allowed in concurrent programs. Understanding the JMM is essential for writing correct concurrent code and avoiding subtle bugs related to visibility and ordering.
Key Concepts
Memory Architecture
┌─────────────────────────────────────────────────────────────┐
│ Java Memory Architecture │
├─────────────────────────────────────────────────────────────┤
│ │
│ Thread 1 Thread 2 Thread 3 │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ Local │ │ Local │ │ Local │ │
│ │ Cache │ │ Cache │ │ Cache │ │
│ └───┬───┘ └───┬───┘ └───┬───┘ │
│ │ │ │ │
│ │ Flush/ │ Flush/ │ │
│ │ Refresh │ Refresh │ │
│ │ │ │ │
│ ────┴─────────────────┴───────────────────┴──── │
│ │ │
│ ┌────┴────┐ │
│ │ Main │ │
│ │ Memory │ │
│ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Happens-Before Relationship
The happens-before relationship guarantees that memory writes by one statement are visible to another.
Key happens-before rules:
- Program Order Rule: Each action in a thread happens-before every subsequent action in that thread
- Monitor Lock Rule: Unlock happens-before subsequent lock of same monitor
- Volatile Variable Rule: Write to volatile happens-before subsequent read
- Thread Start Rule: Thread.start() happens-before any action in started thread
- Thread Termination Rule: Any action in thread happens-before Thread.join()
- Transitivity: If A happens-before B, and B happens-before C, then A happens-before C
Best Practices
1. Use Volatile for Simple Flags
For simple read-write flags shared between threads, volatile is sufficient.
2. Prefer Atomic Classes for Counters
Use AtomicInteger, AtomicLong, etc. for thread-safe counters.
3. Use Final Fields for Immutability
Final fields guarantee visibility after construction.
4. Avoid Publishing ‘this’ in Constructors
The object may be seen in a partially constructed state.
5. Synchronize Access to All Mutable Shared State
Both reads and writes need synchronization.
Code Examples
Example 1: Volatile Visibility
public class VolatileExample {
// Without volatile - may never see update
private boolean stopRequested = false;
// With volatile - guaranteed visibility
private volatile boolean stopRequestedSafe = false;
// BROKEN - thread may never stop
public void brokenExample() {
new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++; // JIT may hoist the check out of loop
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
stopRequested = true; // May never be seen by other thread
}
// CORRECT - with volatile
public void correctExample() {
new Thread(() -> {
int i = 0;
while (!stopRequestedSafe) {
i++; // Will check volatile each iteration
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
stopRequestedSafe = true; // Guaranteed to be visible
}
// Volatile is NOT atomic for compound operations
private volatile int counter = 0;
public void brokenIncrement() {
counter++; // NOT atomic! Read-modify-write
}
// Use AtomicInteger instead
private final AtomicInteger atomicCounter = new AtomicInteger(0);
public void correctIncrement() {
atomicCounter.incrementAndGet(); // Atomic operation
}
}
Example 2: Safe Publication
public class SafePublication {
// UNSAFE - other threads may see partially constructed Holder
public Holder unsafeHolder;
// SAFE - volatile guarantees visibility of fully constructed object
public volatile Holder volatileHolder;
// SAFE - final guarantees visibility after construction
public final Holder finalHolder;
// SAFE - synchronized access
private Holder synchronizedHolder;
public SafePublication() {
this.finalHolder = new Holder(42);
}
public synchronized Holder getSynchronizedHolder() {
return synchronizedHolder;
}
public synchronized void setSynchronizedHolder(Holder holder) {
this.synchronizedHolder = holder;
}
// Immutable objects are always thread-safe to publish
public record ImmutableHolder(int value, String name, List<String> items) {
public ImmutableHolder {
// Defensive copy for safe publication
items = List.copyOf(items);
}
}
// Safe initialization holder idiom
private static class LazyHolder {
static final ExpensiveObject INSTANCE = new ExpensiveObject();
}
public static ExpensiveObject getInstance() {
return LazyHolder.INSTANCE; // Thread-safe lazy initialization
}
}
class Holder {
private final int value;
public Holder(int value) {
this.value = value;
}
public void assertSanity() {
if (value != value) { // Can fail without safe publication!
throw new AssertionError("Sanity check failed");
}
}
}
Example 3: Memory Barriers and Ordering
public class MemoryBarriers {
private int a = 0;
private int b = 0;
private volatile boolean flag = false;
// Without proper synchronization, reordering is possible
public void writer() {
a = 1; // (1)
b = 2; // (2)
flag = true; // (3) - Volatile write is a release barrier
// Volatile write prevents reordering of (1), (2) after (3)
// Reader is guaranteed to see a=1, b=2 after seeing flag=true
}
public void reader() {
if (flag) { // Volatile read is an acquire barrier
// Guaranteed to see a=1, b=2
assert a == 1;
assert b == 2;
}
}
// Using VarHandle for explicit memory ordering (Java 9+)
private static final VarHandle FLAG_HANDLE;
private boolean flagWithVarHandle = false;
static {
try {
FLAG_HANDLE = MethodHandles.lookup()
.findVarHandle(MemoryBarriers.class, "flagWithVarHandle", boolean.class);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
public void writerWithVarHandle() {
a = 1;
b = 2;
// Release semantics - all prior writes visible
FLAG_HANDLE.setRelease(this, true);
}
public void readerWithVarHandle() {
// Acquire semantics - subsequent reads see prior writes
if ((boolean) FLAG_HANDLE.getAcquire(this)) {
assert a == 1;
assert b == 2;
}
}
// Opaque access - no reordering, but no synchronization
public void opaqueAccess() {
FLAG_HANDLE.setOpaque(this, true); // No reordering
boolean val = (boolean) FLAG_HANDLE.getOpaque(this);
}
}
Example 4: Final Fields Semantics
public class FinalFieldsSemantics {
// Final fields have special publication guarantees
public class SafelyPublished {
private final int x;
private final Object ref;
private final int[] array;
public SafelyPublished(int x, Object ref, int[] array) {
this.x = x;
this.ref = ref;
// Make defensive copy for arrays
this.array = Arrays.copyOf(array, array.length);
}
// After construction, other threads are guaranteed to see
// correctly initialized final fields (even without synchronization)
}
// UNSAFE - 'this' escapes before construction completes
public class UnsafeEscape {
private final int value;
public UnsafeEscape(EventSource source) {
source.registerListener(
e -> doSomething(value) // 'this' escapes!
);
value = 42; // May not be visible when listener is called
}
}
// SAFE - don't let 'this' escape
public class SafeConstruction {
private final int value;
private final EventListener listener;
private SafeConstruction(int value) {
this.value = value;
this.listener = e -> doSomething(this.value);
}
// Factory method registers listener after construction
public static SafeConstruction create(int value, EventSource source) {
SafeConstruction instance = new SafeConstruction(value);
source.registerListener(instance.listener);
return instance;
}
}
// Final field freeze extends to objects reachable from final field
public class DeepFreeze {
private final Map<String, Config> configs;
public DeepFreeze(Map<String, Config> configs) {
// Defensive copy
this.configs = Map.copyOf(configs);
}
// Thread reading this object after publication will see
// fully initialized Map AND all Config objects within it
}
}
Example 5: Double-Checked Locking Done Right
public class DoubleCheckedLocking {
// BROKEN without volatile
private static Object unsafeInstance;
public static Object getUnsafeInstance() {
if (unsafeInstance == null) {
synchronized (DoubleCheckedLocking.class) {
if (unsafeInstance == null) {
unsafeInstance = new Object(); // May see partially constructed
}
}
}
return unsafeInstance; // May return partially constructed object!
}
// CORRECT with volatile
private static volatile Object safeInstance;
public static Object getSafeInstance() {
Object result = safeInstance;
if (result == null) {
synchronized (DoubleCheckedLocking.class) {
result = safeInstance;
if (result == null) {
safeInstance = result = new Object();
}
}
}
return result;
}
// BETTER - Initialization-on-demand holder idiom
private static class InstanceHolder {
static final ExpensiveObject INSTANCE = new ExpensiveObject();
}
public static ExpensiveObject getInstance() {
return InstanceHolder.INSTANCE;
}
// BEST for Java 9+ - use VarHandle
private static Object varHandleInstance;
private static final VarHandle INSTANCE_HANDLE;
static {
try {
INSTANCE_HANDLE = MethodHandles.lookup()
.findStaticVarHandle(DoubleCheckedLocking.class, "varHandleInstance", Object.class);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
public static Object getInstanceWithVarHandle() {
Object result = (Object) INSTANCE_HANDLE.getAcquire();
if (result == null) {
synchronized (DoubleCheckedLocking.class) {
result = (Object) INSTANCE_HANDLE.getAcquire();
if (result == null) {
result = new Object();
INSTANCE_HANDLE.setRelease(result);
}
}
}
return result;
}
}
Anti-Patterns
❌ Assuming Visibility Without Synchronization
// WRONG - no visibility guarantee
private boolean done = false;
private int result;
public void compute() {
result = heavyComputation();
done = true; // No guarantee other threads see this
}
public int getResult() {
while (!done); // May spin forever
return result; // May see stale value
}
❌ Non-Atomic Read-Modify-Write on Volatile
// WRONG - not atomic
private volatile int counter = 0;
public void increment() {
counter++; // Three operations: read, increment, write
}
❌ Incorrect Lazy Initialization
// WRONG - race condition
private Object instance;
public Object getInstance() {
if (instance == null) {
instance = new Object(); // Multiple threads may create
}
return instance;
}
Testing Strategies
Testing Memory Visibility
@Test
void shouldEnsureVisibilityWithVolatile() throws Exception {
AtomicBoolean sawUpdate = new AtomicBoolean(false);
VolatileFlag flag = new VolatileFlag();
Thread reader = new Thread(() -> {
while (!flag.isSet()) {
// Spin
}
sawUpdate.set(true);
});
reader.start();
Thread.sleep(100); // Give reader time to start
flag.set();
reader.join(1000);
assertThat(sawUpdate.get()).isTrue();
assertThat(reader.isAlive()).isFalse();
}
// JCStress for exhaustive concurrency testing
@JCStressTest
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "Both see update")
@Outcome(id = "0, 0", expect = Expect.FORBIDDEN, desc = "Neither sees update")
@State
public class VolatileVisibilityTest {
volatile int x;
int y;
@Actor
public void writer() {
y = 1;
x = 1;
}
@Actor
public void reader(II_Result r) {
r.r1 = x;
r.r2 = y;
}
}