← Back to list

Understanding Java Memory Model: How I Debugged a Concurrency Bug Without Locks

Digging deep into volatile, instruction reordering, and the JVM Memory Model to fix an elusive race condition.

Hash Block · 2025-07-14 16:38 · 53 claps · 2.3 min read
#java #concurrency #jmm #volatile #threading
Open on Medium ↗
Wiki topics: SOC · Sociology & Politics 📚 · Books & Reading

Understanding Java Memory Model: How I Debugged a Concurrency Bug Without Locks

Digging deep into volatile, instruction reordering, and the JVM Memory Model to fix an elusive race condition.

🧩 Introduction

Debugging concurrency bugs is like chasing ghosts. I recently hit a case where a shared flag updated by one thread wasn’t visible to another — despite all logic saying it should be.

Turns out, I was fighting the Java Memory Model (JMM) — and I won. In this article, I’ll walk you through how I diagnosed and fixed this subtle issue without using locks, and how understanding volatile, memory barriers, and reordering rules can save you hours.

🧠 Background: The Setup

Here’s the simplified version of what I had:

class SharedData {
    boolean ready = false;
    int value = 0;
}

Thread 1: Writer

data.value = 42;
data.ready = true;

Thread 2: Reader

if (data.ready) {
    System.out.println(data.value); // Sometimes prints 0!
}

How is that possible? Didn’t we write value = 42 before setting ready = true?

🔬 The Java Memory Model in Action

The JMM allows the JVM and CPU to reorder instructions as long as it doesn’t break single-threaded semantics.

So internally, this could happen:

// Actual execution order (possible)
data.ready = true;
data.value = 42;

Now, if thread 2 sees ready = true, it may still see the old value of value.

⚠️ What Didn’t Work

I first tried:

synchronized (data) {
    data.value = 42;
    data.ready = true;
}

But synchronizing only the write side isn’t enough. Thread 2 still reads unsafely.

✅ The Fix: volatile

class SharedData {
    volatile boolean ready = false;
    int value = 0;
}

Declaring ready as volatile solves the problem.

*volatile ensures:*

  1. Visibility: Writes to ready are flushed to main memory.
  2. Ordering: All writes before setting ready = true happen-before any thread reading ready = true.

This effectively acts like a memory barrier.

🧪 Verifying It with a Test

Here’s how I tested it:

public class RaceConditionTest {
    static class Shared {
        volatile boolean ready = false;
        int value = 0;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            Shared shared = new Shared();

            Thread writer = new Thread(() -> {
                shared.value = 42;
                shared.ready = true;
            });

            Thread reader = new Thread(() -> {
                while (!shared.ready);
                if (shared.value != 42) {
                    System.out.println("Race detected: " + shared.value);
                }
            });

            writer.start();
            reader.start();

            try {
                writer.join();
                reader.join();
            } catch (InterruptedException e) {}
        }
    }
}

Without volatile, you will see occasional prints like Race detected: 0. With it — nothing.

📋 Summary Table

Here’s a summary of JMM keywords:

+------------+-------------------------------------------------------------+
| Keyword    | Meaning                                                     |
+------------+-------------------------------------------------------------+
| volatile   | Guarantees visibility + ordering for a single variable      |
| synchronized | Full memory barrier + mutual exclusion                    |
| final      | Guarantees immutability (post-constructor)                 |
+------------+-------------------------------------------------------------+

🧾 Conclusion

Understanding the Java Memory Model is crucial if you’re writing low-latency or lock-free code. This bug taught me that:

  • Instruction reordering is real
  • Visibility is not guaranteed without volatile or synchronized
  • volatile is not a performance hack — it's the correct tool for signaling state across threads

Mastering this will make you a more resilient Java engineer — especially when things go wrong in production.


메타데이터
post_id
fccf6d89ee05
slug
understanding-java-memory-model-how-i-debugged-a-concurrency-bug-without-locks-fccf6d89ee05
url
https://medium.com/@connect.hashblock/understanding-java-memory-model-how-i-debugged-a-concurrency-bug-without-locks-fccf6d89ee05
canonical_url
https://medium.com/@connect.hashblock/understanding-java-memory-model-how-i-debugged-a-concurrency-bug-without-locks-fccf6d89ee05
author_url
https://medium.com/@connect.hashblock
status
ok
fetched_at
2026-07-19 00:05:24