Modern CPUs and compilers may reorder instructions, cache values in registers, or keep thread-local copies of variables in CPU caches. Without explicit synchronisation, Thread A's writes may never become visible to Thread B.
// WARNING: broken — no synchronisation between threads
public class VisibilityBug {
static boolean ready = false;
static int value = 0;
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(() -> {
value = 42;
ready = true; // compiler may reorder: ready = true before value = 42
});
Thread reader = new Thread(() -> {
while (!ready) { /* spin */ }
// reader may see ready == true but value == 0 due to reordering!
System.out.println(value);
});
reader.start();
writer.start();
}
}
The code above has two bugs: (1) ready written by the writer may never be seen by the spinning reader; (2) the compiler or CPU may reorder value = 42 and ready = true, so the reader could see ready == true but value == 0. Both bugs are fixed by declaring volatile or using synchronisation.
If action A happens-before action B, then all effects of A are visible to B. The JMM defines these relationships:
| Rule | happens-before guarantee |
| Program order | Within a single thread, each statement happens-before the next |
| Monitor lock | Unlock of a monitor happens-before every subsequent lock of that monitor |
| Volatile write | A write to a volatile field happens-before every subsequent read of that field |
| Thread start | Thread.start() happens-before any action in the started thread |
| Thread join | Any action in a thread happens-before Thread.join() returns |
| Object initialisation | Static initialiser completion happens-before any thread accesses the class |
| Transitivity | If A hb B and B hb C, then A hb C |
Declaring a field volatile guarantees: (1) writes are immediately flushed to main memory; (2) reads always fetch from main memory; (3) no reordering of volatile accesses with surrounding reads/writes.
public class VolatileExample {
// volatile ensures all threads see the latest value
private volatile boolean ready = false;
private volatile int value = 0;
public void writer() {
value = 42; // 1. write value (not volatile — but...)
ready = true; // 2. volatile write; JMM guarantees: value=42 visible after ready=true
}
public void reader() {
while (!ready) { /* spin */ } // 3. volatile read
// 4. Because ready write hb ready read, value=42 is guaranteed visible here
System.out.println(value); // always prints 42
}
}
A volatile write to field X guarantees that all writes performed by that thread before the volatile write are visible to any thread that subsequently reads X. This is the "piggybacking" rule — you can use a single volatile to publish non-volatile state.
volatile guarantees visibility but not atomicity. Compound operations like i++ (read-modify-write) are not atomic even on volatile fields.
public class VolatileLimitation {
volatile int counter = 0;
// BROKEN: multiple threads calling this will lose updates
// i++ compiles to: read counter, increment, write counter (3 steps — not atomic)
public void increment() {
counter++;
}
// CORRECT: use AtomicInteger for atomic compound operations
java.util.concurrent.atomic.AtomicInteger atomicCounter = new java.util.concurrent.atomic.AtomicInteger(0);
public void incrementCorrectly() {
atomicCounter.incrementAndGet(); // single atomic CAS operation
}
}
| Tool | Visibility | Atomicity | Mutual exclusion |
volatile | ✅ | Simple read/write only | ❌ |
synchronized | ✅ | ✅ (entire block) | ✅ |
AtomicInteger etc. | ✅ | ✅ (single variable) | ❌ |
ReentrantLock | ✅ | ✅ (entire block) | ✅ |
Acquiring a monitor lock establishes a happens-before with the preceding unlock of that same monitor. Everything the previous lock holder wrote is visible to the next acquirer.
public class SynchronizedVisibility {
private int value = 0;
private final Object lock = new Object();
// All writes inside synchronized are flushed when lock is released
public synchronized void write(int v) {
value = v;
} // unlock happens here — flushes value to main memory
// All reads inside synchronized see the latest written value
public synchronized int read() {
return value;
} // unlock on exit
// Equivalent explicit lock
public void writeWithLock(int v) {
synchronized (lock) {
value = v;
} // unlock — happens-before any subsequent lock on 'lock'
}
}
An object is safely published when all its fields are visible to any thread that obtains a reference to it. Unsafe publication allows a thread to see a partially constructed object.
// UNSAFE: another thread might see a partially constructed Config
public class UnsafePublication {
public static Config instance; // plain field, not volatile
public static void init() {
instance = new Config(); // reference may be visible before constructor completes
}
}
// SAFE option 1: volatile field
public class VolatilePublication {
public static volatile Config instance; // volatile write hb volatile read
public static void init() {
instance = new Config();
}
}
// SAFE option 2: final fields — immutable objects are always safely published
public class SafeImmutable {
public final int x;
public final int y;
public SafeImmutable(int x, int y) {
this.x = x;
this.y = y;
}
// final fields guaranteed visible after constructor returns — even without volatile
}
// SAFE option 3: static initialiser (class loading is thread-safe)
public class SafeSingleton {
private static final Config INSTANCE = new Config(); // guaranteed safe
public static Config get() { return INSTANCE; }
}
// SAFE option 4: double-checked locking with volatile (Java 5+)
public class DCLSingleton {
private static volatile DCLSingleton instance;
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
The final field rule in the JMM (Java 5+) guarantees that after a constructor completes normally, any thread that obtains a reference to the object sees the correct values of final fields without synchronisation — even if the reference was published unsafely.
The JVM and CPU may reorder instructions as long as the program appears correct within a single thread. Across threads, only happens-before rules prevent reordering.
| Reordering type | Example | Prevented by |
| Compiler reorder | Hoisting a read out of a loop | volatile, synchronized |
| CPU store buffer | Write visible to local CPU before other CPUs | volatile (StoreLoad memory barrier) |
| CPU instruction reorder | Independent instructions swapped for pipeline efficiency | volatile, synchronized |
| Read caching | CPU serves read from L1 cache instead of main memory | volatile forces cache invalidation |
// volatile fields create memory barriers
// Writing a volatile field: StoreStore + StoreLoad barriers inserted
// Reading a volatile field: LoadLoad + LoadStore barriers inserted
volatile int flag = 0;
// Thread A
a = 1; // ordinary write
b = 2; // ordinary write (both guaranteed to happen before flag write)
flag = 1; // volatile write ← StoreStore barrier before; StoreLoad after
// Thread B
int f = flag; // volatile read ← LoadLoad barrier before; LoadStore after
// Any read of a, b here is guaranteed to see a=1, b=2
int x = a;
int y = b;
| Pitfall | Symptom | Fix |
| Non-volatile stop flag | Thread never sees running = false | Declare volatile boolean running |
| Unsafe lazy init (no volatile) | Reader sees partially constructed object | Use volatile, final, or static holder |
volatile for compound actions | Lost updates on i++ | Use AtomicInteger or synchronized |
| Synchronising on different objects | No mutual exclusion between threads | Always lock the same object |
| Publishing via non-final, non-volatile field | Other threads see null or stale reference | Publish via volatile or final |
| Assuming sequential consistency | Counter-intuitive orderings on multi-core | Understand happens-before; don't assume execution order |