The Java Concurrency Trap That Many Developers Fall Into

The Java Concurrency Trap That Many Developers Fall Into

I posted this quiz a few days ago on LinkedIn, and the results were fascinating.

https://www.linkedin.com/feed/update/urn:li:activity:7348845601058365440/

With 56 votes in, here's how developers answered:

  • 66% thought the program would "Run forever."
  • 18% believed it would "Run for 1 sec then terminate."
  • 14% said, "Depends on the JVM."
  • 2% expected it to "Throw an exception."

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:

  1. Each thread can have its own cached copy of variables
  2. Changes made by one thread might not be visible to other threads
  3. The JVM is free to optimize code in ways that assume single-threaded execution

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:

  • With -Xint (interpreted mode), the program might actually terminate
  • On single-core machines, context switching might make the change visible
  • Some JVM implementations might be more aggressive about cache synchronization

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:

  • Writes to the variable are immediately flushed to main memory
  • Reads always fetch the latest value from main memory
  • It establishes a happens-before relationship

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:

  1. Using synchronized methods:

private static synchronized void requestStop() {
    stopRequested = true;
}

private static synchronized boolean stopRequested() {
    return stopRequested;
}        

  1. Using AtomicBoolean:

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?

Pavly Gerges

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/

Like
Reply

To view or add a comment, sign in

Others also viewed

Explore topics