Explanation
What just happened
main set stop = true, but worker kept spinning. The loop was hot, so the JIT compiled it, and because stop is a plain field, the compiled code was allowed to read it once and reuse the value. worker never looked again, and join() never returned.
Try the what-if switch
volatile: a volatile field must be read from memory every time, and every write to it is visible to later reads on any thread. worker saw true on its next lap and exited.
Why Java allows this
Without volatile or a lock, the Java Memory Model doesn’t promise that one thread ever sees another’s write. That freedom lets the JIT and CPU keep values in registers and caches, which is what makes ordinary code fast. You opt in to cross-thread visibility where you need it.
Things that make a write visible:
volatilefieldssynchronizedblocks on the same lock (unlocking publishes, locking reads fresh)Thread.start()andjoin()java.util.concurrentclasses likeAtomicBooleanandBlockingQueue
Watch out
volatile fixes visibility, not atomicity. count++ on a volatile field is still read-add-write, so it still loses updates. Use it for flags written by one thread and read by others.
Common questions
Why does my thread never see a flag set by another thread?
Without volatile or a lock, the Java Memory Model doesn't promise one thread ever sees another's write. In practice the JIT compiles the hot loop to read the field once and reuse the value.
What does volatile do in Java?
Every read of a volatile field sees the latest write from any thread. That's why a loop checking a volatile stop flag exits on its next lap.
Is volatile enough for count++?
No. volatile fixes visibility, not atomicity. Use AtomicInteger or a lock for read-modify-write.
Verified: this timeline matches a real program, checked in CI on JDK 21 and 25. How we verify. Unclear or wrong? Tell us