The JMM defines when one thread's write becomes visible to another. Without proper synchronization, modern CPUs and compilers can reorder operations in ways that look impossible to a single-threaded reader. Understanding happens-before is the difference between code that 'works on my machine' and code that works in production.
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.
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.
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*.