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:

Rulehappens-before guarantee
Program orderWithin a single thread, each statement happens-before the next
Monitor lockUnlock of a monitor happens-before every subsequent lock of that monitor
Volatile writeA write to a volatile field happens-before every subsequent read of that field
Thread startThread.start() happens-before any action in the started thread
Thread joinAny action in a thread happens-before Thread.join() returns
Object initialisationStatic initialiser completion happens-before any thread accesses the class
TransitivityIf 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 } }
ToolVisibilityAtomicityMutual exclusion
volatileSimple 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 typeExamplePrevented by
Compiler reorderHoisting a read out of a loopvolatile, synchronized
CPU store bufferWrite visible to local CPU before other CPUsvolatile (StoreLoad memory barrier)
CPU instruction reorderIndependent instructions swapped for pipeline efficiencyvolatile, synchronized
Read cachingCPU serves read from L1 cache instead of main memoryvolatile 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;
PitfallSymptomFix
Non-volatile stop flagThread never sees running = falseDeclare volatile boolean running
Unsafe lazy init (no volatile)Reader sees partially constructed objectUse volatile, final, or static holder
volatile for compound actionsLost updates on i++Use AtomicInteger or synchronized
Synchronising on different objectsNo mutual exclusion between threadsAlways lock the same object
Publishing via non-final, non-volatile fieldOther threads see null or stale referencePublish via volatile or final
Assuming sequential consistencyCounter-intuitive orderings on multi-coreUnderstand happens-before; don't assume execution order