Loop doesn't see value changed by other thread without a print statement
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
If a Java loop only stops when you add System.out.println, the real bug is not in printing. It is a memory-visibility problem: one thread updates a value, but another thread is allowed to keep reading a stale copy because the code has no proper happens-before relationship.
Why the Loop Can Miss the Update
In Java, threads do not automatically see each other's writes immediately. The Java Memory Model allows:
- CPU caching
- JIT optimizations
- Reordering of reads and writes
That means a loop like this is broken:
The worker thread is not guaranteed to observe the write to done. It may keep reading a cached value or allow the JIT to optimize the loop in a way that assumes done does not change.
Why println Seems to Fix It
System.out.println changes timing, and it also synchronizes internally because PrintStream methods are synchronized. That extra synchronization can create enough memory barriers to make the updated value visible by accident.
This is why adding logging sometimes makes a concurrency bug "go away." The code was never correct. The print statement only changed the execution conditions.
The Correct Fix: volatile
If the variable is a simple shared flag, mark it volatile:
volatile gives you two important guarantees:
- A write by one thread becomes visible to readers in other threads
- Reads and writes of that variable are not freely reordered across the visibility barrier
For a stop flag, that is usually exactly what you need.
When volatile Is Not Enough
volatile works for visibility, but it does not make compound operations atomic. For example, this is still unsafe:
count++ is read, modify, write. Multiple threads can still race. In that case, use synchronization or an atomic type:
Other Correct Approaches
You can also fix visibility with:
- '
synchronizedblocks or methods' - '
Lockimplementations fromjava.util.concurrent.locks' - Higher-level concurrency constructs such as
CountDownLatch
For example, a latch expresses one-time completion much more cleanly than a spin loop:
Common Pitfalls
- Assuming a debug print proves the code is correct.
- Using a plain shared boolean between threads without
volatileor synchronization. - Using
volatilefor non-atomic compound operations and expecting it to solve race conditions. - Writing busy loops when a higher-level synchronization primitive would express the intent more clearly.
Summary
- A loop missing another thread's update is a visibility bug.
- '
printlnonly changes timing and synchronization side effects; it is not a real fix.' - Use
volatilefor simple shared flags. - Use atomics or locks for compound updates.
- In many cases, a latch, queue, or other concurrency primitive is better than a spin loop.

