The Java Concurrency Trap That Many Developers Fall Into
I posted this quiz a few days ago on LinkedIn, and the results were fascinating.
With 56 votes in, here's how developers answered:
The code snippet is like this:
import java.time.Duration;
public class StopTheThread {
private static boolean stopRequested;
void main() throws InterruptedException {
Thread.ofPlatform().start(()->{
int i = 0;
while (!stopRequested)
i++;
});
Thread.sleep(Duration.ofSeconds(1));
stopRequested = true;
}
}
Let's unpack what's really going on here and why so many of them got it wrong (or rather, got it right for the wrong reasons).
The Memory Visibility Problem
At first glance, the code seems straightforward: a thread increments a counter while checking a stopRequested flag, and after 1 second, the main thread sets the flag to true. Most developers reasonably expect this would stop the loop after 1 second.
However, 66% of respondents were correct—this program will most likely run forever. Here's why:
The Missing volatile Keyword
The core issue is that stopRequested is not declared as volatile. In Java's memory model, without proper synchronization:
When the main thread sets stopRequested = true, this change happens in the main thread's cache. The newly created thread, running on potentially a different CPU core, continues reading its cached copy where stopRequested is still false.
What Joshua Bloch Says
This exact pattern is discussed in Effective Java, Item 78: "Synchronize access to shared mutable data". I, in fact, used the same example he used in the book and warned:
"In the absence of synchronization, there is no guarantee as to when, if ever, the background thread will see the change made by the main thread."
He goes on to explain that the JVM might even transform the code:
while (!stopRequested)
i++;
Into:
if (!stopRequested)
while (true)
i++;
This optimization, called "hoisting," is legal under the Java Memory Model because the JVM assumes no other thread will modify stopRequested.
But if we use the volatile keyword, the JVM wouldn't make such an optimization.
Why Some Chose "Depends on the JVM"
The 14% who selected "Depends on the JVM" were actually being quite astute. Different JVM implementations, optimization levels, and hardware architectures can affect behavior:
However, according to the Java Memory Model specification, without proper synchronization, there's no guarantee the change will ever be visible.
The Fix
The solution is simple; declare the flag as volatile:
private static volatile boolean stopRequested;
The volatile keyword ensures:
As Bloch notes in Effective Java, volatile performs no mutual exclusion, but it guarantees that any thread that reads the field will see the most recently written value.
Alternative Solutions
Effective Java also presents other solutions to this problem:
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
private static final AtomicBoolean stopRequested = new AtomicBoolean();
What's your experience with these types of concurrency issues? Have you encountered similar visibility problems in production code?
C | Modern C++ | Java | Embedded Systems | Embedded Linux | MCUs Programming | Android SDK & NDK | Circuit prototyping | KiCad | Discrete Mathematics | Simulation and Modelling | jMonkeyEngine Game Engine | Medicine
1mo`What's your experience with these types of concurrency issues? Have you encountered similar visibility problems in production code?` Yep, here, I built this API to overcome concurrency issues in embedded-controllable game environments using dependency injection and game loop patterns: * https://github.com/Electrostat-Lab/Jector/