The problem

Thread A: data = 1; flag = true;. Thread B: if (flag) { use(data); }. Without synchronization, thread B may see flag=true but data=0 — the compiler or CPU reordered A's writes. Plain reads/writes have NO happens-before relationship across threads.

Advertisement

volatile

volatile int flag: writes to flag have happens-before relationship with subsequent reads of flag by other threads. Also: writes BEFORE the volatile write are visible to reads AFTER the volatile read. This single keyword fixes the example above.

Advertisement

synchronized

Entering a synchronized block has happens-before relationship with exiting the same monitor by another thread. Inside the block, all reads see latest writes from previous holders. Stronger than volatile (covers compound operations) but slower.

happens-before rules

Single-thread: program order → happens-before. Monitor: unlock happens-before next lock. Volatile: write happens-before read. Thread start: Thread.start() happens-before first action of started thread. Thread join: last action of joined thread happens-before join() return.

The takeaway

If thread A writes X and thread B reads X without a happens-before relationship between them, B can see ANY value (including a partial write on long/double). Always synchronize or use volatile. Lock-free code requires explicit happens-before via VarHandle or Atomic*.