Every Java developer eventually meets a bug that makes no sense. A flag is set to true in one thread, yet another thread keeps looping as if nothing happened. The code looks correct. The logic is sound. Yet the program hangs, or worse, behaves differently on different machines. These are visibility bugs, and the Java Memory Model (JMM) is the specification that explains why they happen and how to prevent them. Java Training in Chennai at FITA Academy can help learners understand concepts such as thread visibility, synchronization, volatile variables, and happens-before relationships.
The Illusion of a Single Shared Memory
When we write multithreaded code, we tend to picture one block of memory that all threads read and write in a clear, orderly sequence. Real hardware does not work that way. Each CPU core has its own caches and store buffers, and writes may sit in those buffers before reaching main memory. On top of that, compilers and processors reorder instructions to squeeze out performance, as long as the result looks unchanged from the perspective of a single thread.
That last phrase is the key. Optimizations are only required to preserve behavior as seen by one thread. The moment a second thread observes the same data, the guarantees disappear unless the program says otherwise. The JMM exists to define exactly what a program must do to get predictable behavior across threads.
What Visibility Actually Means
Visibility answers a simple question. When thread A writes a value, when is thread B guaranteed to see it? Without any synchronization, the answer is never. Thread B might see the new value immediately, after a delay, or not at all. It might even see some writes from thread A but not others, in a different order than they were written.
This is why a stop flag without proper declaration can cause an infinite loop. The compiler or runtime is allowed to read the variable once, keep it in a register, and never check it again. From the single threaded point of view this is a perfectly valid optimization. From the multithreaded point of view it is a bug.
Happens-Before: The Core Idea
The JMM does not describe caches or registers. Instead it defines a relationship called happens-before. If one action happens-before another, then the results of the first are guaranteed to be visible to the second, and the first is ordered before it.
The specification lists several situations that create this relationship:
- An unlock of a monitor happens-before every later lock of that same monitor.
- A write to a volatile variable happens-before every later read of that variable.
- A call to start a thread happens-before any action in the started thread.
- All actions in a thread happen-before another thread successfully returns from joining it.
- Within a single thread, each action happens-before the next one in program order.
These rules are transitive. If A happens-before B and B happens-before C, then A happens-before C. This transitivity is what makes the model practical, because one well placed synchronization point can publish a whole set of earlier writes.
Volatile and Synchronized
The volatile keyword is the lightest tool for visibility. Declaring a variable volatile tells the runtime that every write must be visible to subsequent reads from any thread, and that the compiler cannot cache it or reorder around it in unsafe ways. It is ideal for simple state flags, such as a shutdown signal or a status indicator.
Volatile has limits, though. It guarantees visibility and ordering, but it does not make compound operations atomic. Incrementing a volatile counter is still a read, a modify, and a write, and two threads can interleave those steps and lose updates. For that we need atomic classes or locking.
Synchronized blocks provide both mutual exclusion and visibility. When a thread exits a synchronized block, its writes are flushed in a way that any thread entering a block on the same monitor will see them. This is why shared mutable state guarded by a lock does not suffer from stale reads, provided every access uses the same lock. A single unguarded read is enough to bring the problem back.
Safe Publication and Final Fields
Visibility bugs often hide in object construction. If a reference to a new object is shared without synchronization, another thread may see the reference but observe the fields in a partially initialized state. This is the root of many broken lazy initialization patterns, including the infamous flawed double checked locking idiom, which only works correctly when the shared reference is volatile.
The JMM offers a special guarantee for final fields. Once a constructor finishes, any thread that sees the reference will see the correct values of its final fields, without extra synchronization, as long as the reference did not escape during construction. This is one of the strongest arguments for immutable objects, which are naturally safe to share across threads.
Practical Habits That Prevent These Bugs
Understanding the model leads to a few reliable habits:
- Prefer immutable data wherever possible.
- Use the concurrency utilities in java.util.concurrent instead of hand-built coordination.
- Mark shared flags as volatile, and use atomic classes for counters.
- Guard shared mutable state with one consistent lock.
- Never let a reference escape from a constructor.
- Treat a passing test as weak evidence, since visibility bugs depend on timing and hardware.
That last point deserves emphasis. These bugs are notoriously hard to reproduce. A program may run flawlessly on a laptop for months and fail under load on a production server with more cores. Reasoning with happens-before is far more reliable than testing alone.
The Java Memory Model can seem abstract, but its message is simple. Threads do not automatically see each other's work. Visibility must be established explicitly through synchronization, volatile access, or safe publication. Once we learn to ask whether a happens-before relationship exists between a write and a read, the mysterious hangs and stale values stop being mysterious. They become predictable consequences of a well defined contract, and that makes them far easier to avoid.