Java Memory Model & happens-before

JMM ও happens-before — thread-safe কোডের পদার্থবিদ্যা

Read: ~35 min Advanced 5 practice problems Live code runner

1. The Problem — What Do Threads Actually See?

Modern CPUs have multiple cores, each with its own cache. When Thread A writes a value to a field, that write may live in A's L1 cache for milliseconds before it ever reaches main memory — meaning Thread B on another core might never see it, or might see a stale copy. The compiler is also free to reorder instructions for performance, as long as the single-threaded semantics of the program are preserved.

আধুনিক CPU-তে প্রতিটি core-এর নিজস্ব cache থাকে। Thread A যখন একটি field-এ মান লেখে, সেটি A-র L1 cache-এ থেকে যেতে পারে — Thread B তখন পুরনো (stale) মান দেখতে পায়। Compiler-ও performance-এর জন্য instruction reorder করতে পারে, যতক্ষণ single-threaded ব্যবহারে ফলাফল সঠিক থাকে। Multi-thread code-এ এই reorder ভয়াবহ bug তৈরি করতে পারে।

The Java Memory Model (JMM), defined in the Java Language Specification (JLS §17.4), is the rulebook that answers the question: under exactly what conditions is a write by one thread guaranteed to be visible to a read by another thread? The answer is: only when a happens-before relationship exists between the write and the read.

Core rule: Writing concurrent code in Java without understanding the JMM is shooting arrows in the dark. You may get lucky on your laptop and crash in production — because your laptop's x86 CPU has a stronger memory model than the JMM requires.

JMM না বুঝে concurrent code লেখা — আপনার laptop-এর x86 CPU-তে ঠিক চলবে (কারণ x86 already strong), production-এর ARM server-এ crash করবে।

2. happens-before — The Six Rules

The JMM defines happens-before as a partial ordering on all memory operations. If action A happens-before action B, then every memory write visible to A is guaranteed to be visible to B. The JMM specifies exactly which pairs of actions have this relationship:

JMM সব memory operation-এর উপর একটি partial ordering সংজ্ঞায়িত করে — এটিই happens-before। যদি A happens-before B হয়, তাহলে A-র সব write B-র কাছে দৃশ্যমান হবে। নিচে ছয়টি মূল rule আছে।
#Ruleবাংলায়
1 Program order. Each action in a thread happens-before every subsequent action in the same thread. একই thread-এর মধ্যে আগের action পরের action-এর আগে ঘটে (trivially)।
2 Monitor lock. An unlock of a monitor happens-before every subsequent lock of that same monitor. একটি monitor-এর unlock সেই monitor-এর পরের lock-এর আগে ঘটে।
3 Volatile write. A write to a volatile field happens-before every subsequent read of that same field. volatile field-এ write পরবর্তী সব read-এর আগে ঘটে।
4 Thread start. A call to Thread.start() happens-before any action in the started thread. Thread.start() call সেই thread-এর যেকোনো কাজের আগে ঘটে।
5 Thread termination. Any action in a thread happens-before any other thread detects that thread has terminated (via join()). Thread-এর শেষ কাজ join()-এর রিটার্নের আগে ঘটে।
6 Transitivity. If A happens-before B and B happens-before C, then A happens-before C. A → B এবং B → C হলে A → C।
happens-before guarantees visibility across threads Thread A write x = 42 unlock(monitor) ↑ happens-before lock Thread B lock(monitor) read x → 42 ✓ guaranteed visible Thread C (no sync) read x → ??? could see 0 or 42 no happens-before ! Figure 41.1 — unlock happens-before lock → Thread B-র read guaranteed। Thread C-র কোনো guarantee নেই।

3. Reordering — The Invisible Enemy

Both the compiler and the CPU can reorder memory operations as long as single-threaded behavior is unchanged. In a multi-threaded context this produces surprising results. The canonical example is a flag-based stop signal:

Compiler এবং CPU উভয়েই single-threaded behavior ঠিক রেখে memory operation reorder করতে পারে। Multi-threaded context-এ এটি অপ্রত্যাশিত ফলাফল দেয়। নিচে classic উদাহরণ — volatile ছাড়া flag কাজ নাও করতে পারে।
Main.java — volatile ছাড়া (ভুল)
// WARNING: this may loop forever on server JVMs — demo only
class Broken {
    static boolean done = false; // no volatile

    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(() -> {
            while (!done) { /* spin */ }
            System.out.println("worker sees done=true");
        });
        worker.start();
        Thread.sleep(50);
        done = true; // write may never reach worker's cache
        worker.join(500);
        System.out.println("done (may not have stopped)");
    }
}
Why it breaks: The JVM is allowed to hoist the read of done out of the loop and cache it in a register. Without a happens-before edge between the writer and the reader, the JMM gives zero visibility guarantee — the worker may spin forever.

কেন ভাঙে: JVM done-এর read loop থেকে বাইরে নিয়ে register-এ রাখতে পারে। writer ও reader-এর মধ্যে happens-before না থাকলে JMM কোনো visibility guarantee দেয় না।

4. volatile — Visibility Without Locking

Adding volatile to a field tells the JMM to establish a happens-before edge between every write and every subsequent read of that field. This gives two guarantees:

  • Visibility — a write is immediately flushed to main memory; subsequent reads see it.
  • No reordering — the compiler and CPU cannot move memory operations across a volatile access.
volatile field-এ write করলে সঙ্গে সঙ্গে main memory-তে flush হয় এবং পরবর্তী read সেটি দেখতে পায়। Compiler ও CPU-ও volatile access-এর আশেপাশে instruction reorder করতে পারে না। কিন্তু মনে রাখুন — volatile atomicity দেয় না; শুধু visibility ও ordering দেয়।
Main.java — volatile দিয়ে সঠিক
class Main {
    static volatile boolean done = false; // volatile = happens-before on write/read

    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(() -> {
            while (!done) { /* spin */ }
            System.out.println("worker stopped cleanly");
        });
        worker.start();
        Thread.sleep(30);
        done = true; // JMM guarantees worker sees this
        worker.join();
        System.out.println("main done");
    }
}
volatile write flushes to main memory; volatile read fetches from main memory Thread A cache write volatile done=true Main Memory done = true Thread B cache read volatile done → true ✓ Figure 41.2 — volatile write → main memory flush → পরের volatile read সর্বদা fresh মান পায়।
volatile ≠ atomic. count++ on a volatile int is still a race condition — it is three operations (read, increment, write). For atomic compound operations use AtomicInteger or synchronized.

volatile atomicity দেয় না। volatile int-এ count++ এখনও race condition — এটি তিনটি operation। Compound atomic operation-এর জন্য AtomicInteger বা synchronized ব্যবহার করুন।

5. synchronized & happens-before

Every exit from a synchronized block happens-before every subsequent entry into a synchronized block on the same monitor. This means all writes done while holding the lock are guaranteed to be visible to any thread that later acquires the same lock.

synchronized block থেকে বের হওয়া (unlock) সেই একই monitor-এ পরের entry (lock)-এর আগে happens-before। অর্থাৎ lock hold করে যা লেখা হয়েছে, পরে যে thread lock নেবে সে সব দেখতে পাবে।
Main.java
class SharedCounter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) { count++; }  // unlock happens-before next lock
    }

    public int get() {
        synchronized (lock) { return count; }
    }
}

class Main {
    public static void main(String[] args) throws Exception {
        SharedCounter c = new SharedCounter();

        Thread a = new Thread(() -> { for (int i=0;i<10000;i++) c.increment(); });
        Thread b = new Thread(() -> { for (int i=0;i<10000;i++) c.increment(); });

        a.start(); b.start();
        a.join();  b.join();

        System.out.println("count = " + c.get()); // always 20000
    }
}

6. final Field Semantics

A final field that is properly written in a constructor is guaranteed to be visible to all threads without any synchronization, once the constructor completes. This is one of the most important and often overlooked guarantees in the JMM.

Constructor-এ ঠিকমতো লেখা final field constructor শেষ হওয়ার পর সব thread-এর কাছে দৃশ্যমান হয় — কোনো synchronization ছাড়াই। এটি JMM-এর অনেক গুরুত্বপূর্ণ কিন্তু অবহেলিত guarantee।
Main.java
class ImmutablePoint {
    final int x;
    final int y;

    ImmutablePoint(int x, int y) {
        this.x = x;   // JMM: final write in constructor
        this.y = y;   // safe for all threads once constructor exits
    }
}

class Main {
    static ImmutablePoint sharedPoint; // non-volatile — safe only because of final

    public static void main(String[] args) throws Exception {
        sharedPoint = new ImmutablePoint(10, 20);

        Thread t = new Thread(() -> {
            // Guaranteed to see x=10, y=20 because fields are final
            System.out.println("x=" + sharedPoint.x + " y=" + sharedPoint.y);
        });

        t.start();
        t.join();
    }
}
The safe-publication rule for immutable objects: if all fields of an object are final and correctly set in the constructor, the object can be published to other threads via any mechanism (even a data race on the reference) and the other thread will still see fully initialized fields. This is why String and other immutable Java classes are inherently thread-safe.

সব field final এবং constructor-এ ঠিকমতো set হলে, object-টি যেকোনোভাবে publish করলেও (এমনকি reference-এ data race থাকলেও) অন্য thread initialized field দেখবে। এজন্য String inherently thread-safe।

7. Double-Checked Locking — The Classic Pitfall

Double-checked locking (DCL) is an idiom for lazy initialization of a singleton. The broken version appeared in countless Java textbooks before JDK 5. The fix is a single keyword: volatile.

Double-Checked Locking (DCL) হলো singleton lazy initialization-এর একটি pattern। JDK 5-এর আগে অনেক Java বইতে ভুল version ছিল। সমাধান একটি কীওয়ার্ড — volatile।
Main.java — ভুল DCL (without volatile)
// BROKEN — do NOT use this pattern
class BrokenSingleton {
    private static BrokenSingleton instance;  // missing volatile

    public static BrokenSingleton getInstance() {
        if (instance == null) {             // check 1 — no lock
            synchronized (BrokenSingleton.class) {
                if (instance == null) {     // check 2 — with lock
                    instance = new BrokenSingleton();
                    // BUG: another thread could see instance != null
                    // BEFORE the constructor has finished writing fields
                }
            }
        }
        return instance;
    }
}

class Main {
    public static void main(String[] args) {
        System.out.println("Broken singleton (reference only): " + BrokenSingleton.getInstance());
    }
}
Why it breaks: the JVM is allowed to reorder the write to instance before the constructor body finishes writing the object's fields. Thread B can see a non-null instance that is only partially constructed. Fix: add volatile.

কেন ভাঙে: JVM constructor-এর field write শেষ হওয়ার আগেই instance-এ reference লিখতে পারে। Thread B তখন non-null কিন্তু partially constructed object পায়।
Main.java — সঠিক DCL (volatile)
class Config {
    private volatile static Config instance; // volatile fixes the reorder
    final String host;
    final int    port;

    private Config() { host = "db.bkash.com"; port = 5432; }

    public static Config get() {
        if (instance == null) {
            synchronized (Config.class) {
                if (instance == null)
                    instance = new Config(); // volatile write = full visibility
            }
        }
        return instance;
    }
}

class Main {
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> System.out.println("T1: " + Config.get().host));
        Thread t2 = new Thread(() -> System.out.println("T2: " + Config.get().port));
        t1.start(); t2.start();
        t1.join();  t2.join();
    }
}
Prefer the Initialization-on-Demand Holder (IODH): using an inner static class is simpler, relies only on class-loading guarantees (which include happens-before), and requires no volatile at all.

আরো সহজ বিকল্প — Initialization-on-Demand Holder (IODH): inner static class ব্যবহার করুন। Class loading JMM-এর happens-before guarantee দেয়, তাই volatile-ও লাগে না।
Main.java — IODH Singleton (সবচেয়ে সঠিক)
class AppConfig {
    private AppConfig() {}

    private static class Holder {
        // class loading is thread-safe; no lock, no volatile needed
        static final AppConfig INSTANCE = new AppConfig();
    }

    public static AppConfig get() { return Holder.INSTANCE; }

    public String toString() { return "AppConfig@safe"; }
}

class Main {
    public static void main(String[] args) {
        System.out.println(AppConfig.get());
        System.out.println("same instance? " + (AppConfig.get() == AppConfig.get()));
    }
}

8. Thread start / join — Built-in happens-before

Two more commonly used happens-before guarantees are Thread.start() and Thread.join(). Everything written before start() is visible inside the new thread. Everything done inside a thread is visible to the thread that calls join().

Thread.start() এবং Thread.join() উভয়ই happens-before দেয়। start()-এর আগে লেখা সব কিছু নতুন thread-এ দৃশ্যমান। Thread-এর ভেতরে করা সব কাজ join() করা thread-এ দৃশ্যমান।
Main.java
class Main {
    static int setup = 0;   // no volatile needed — written before start()
    static int result = 0;  // no volatile needed — read after join()

    public static void main(String[] args) throws Exception {
        setup = 42;  // happens-before the thread sees it (rule 4)

        Thread t = new Thread(() -> {
            result = setup * 2;  // sees setup=42 guaranteed
        });

        t.start();
        t.join();  // happens-before we read result (rule 5)

        System.out.println("result = " + result);  // guaranteed 84
    }
}

9. JMM Cheat Sheet

Mechanismhappens-before guaranteeবাংলায়
volatile write → readWrite visible to all subsequent reads of that field.volatile write সব পরবর্তী read-এ দৃশ্যমান।
synchronized unlock → lockAll writes in the critical section visible after next lock.critical section-এর write পরের lock-এ দৃশ্যমান।
Thread.start()Pre-start writes visible inside the thread.start()-এর আগের write thread-এর ভেতরে দৃশ্যমান।
Thread.join()Thread's writes visible to the joining thread.thread-এর write join() করা thread-এ দৃশ্যমান।
final constructor writeFields visible after constructor exits — no sync needed.constructor শেষ হলে final field সব thread-এ দৃশ্যমান।
AtomicX / LockSupportSame guarantee as volatile.volatile-এর মতোই guarantee দেয়।

✅ JMM Do

  • Use volatile for flags shared between threads
  • Use synchronized for compound read-modify-write
  • Prefer immutable objects (final fields) for safe sharing
  • Use Thread.start() / join() for setup / result transfer
  • Use IODH or enum for thread-safe singletons

⚠️ JMM Avoid

  • Bare shared mutable fields without any sync mechanism
  • volatile for compound operations (++, compare-and-swap)
  • Broken DCL (missing volatile on instance)
  • Assuming x86 behavior — JMM is weaker than x86
  • Sharing an object before its constructor finishes

10. Vocabulary (শব্দভাণ্ডার)

TermMeaningবাংলায়
JMMJava Memory Model — rules governing visibility of memory writes across threads.thread-এর মধ্যে memory write-এর visibility নিয়ন্ত্রণকারী নিয়ম।
happens-beforePartial order ensuring that a write is visible to a subsequent read.write-কে পরের read-এর কাছে দৃশ্যমান করার partial order।
ReorderingCompiler/CPU moving memory operations to optimize performance.performance-এর জন্য compiler/CPU-র instruction ক্রম বদল।
volatileKeyword that establishes happens-before on each write/read of the field.প্রতিটি write-read-এ happens-before স্থাপন করে।
Safe publicationPublishing a reference to another thread such that the object is fully visible.object-এর reference অন্য thread-এ এমনভাবে দেওয়া যেন পুরো object দৃশ্যমান হয়।
DCLDouble-Checked Locking — lazy singleton idiom, requires volatile in Java.lazy singleton idiom — Java-তে volatile লাগে।
IODHInitialization-on-Demand Holder — safe singleton via inner static class.inner static class দিয়ে safe lazy singleton।
Data raceTwo threads accessing the same field with no happens-before between them.দুটি thread একই field access করছে কিন্তু তাদের মধ্যে happens-before নেই।

11. Practice Problems

Each problem has a Show Answer button with runnable Java code. Try the problem yourself first, then check.

প্রতিটি প্রশ্নে Show Answer বাটন রয়েছে। প্রথমে নিজে চেষ্টা করুন, তারপর উত্তর মিলিয়ে নিন।
  1. Fix the broken stop-flag program: add volatile to make the worker thread stop correctly.
    নিচের broken stop-flag program ঠিক করুন — volatile যোগ করে worker thread সঠিকভাবে থামান।
    ✨ Show Answer (উত্তর দেখুন)
    Main.java
    class Main {
        static volatile boolean done = false;
    
        public static void main(String[] args) throws Exception {
            Thread worker = new Thread(() -> {
                int spins = 0;
                while (!done) spins++;
                System.out.println("stopped after " + spins + " spins");
            });
            worker.start();
            Thread.sleep(10);
            done = true;
            worker.join();
            System.out.println("main exiting");
        }
    }
  2. Explain in 3 sentences why volatile int count; count++ is still a race condition.
    তিন বাক্যে ব্যাখ্যা করুন — volatile int count; count++ কেন এখনও race condition।
    ✨ Show Answer (উত্তর দেখুন)

    Answer: count++ is a compound operation — read the current value, add 1, write the result back. volatile guarantees that the individual read and write each have happens-before edges, but it does not prevent two threads from both reading the same stale value, both incrementing it to the same new value, and both writing back — losing one increment. To fix this, use AtomicInteger.incrementAndGet(), which performs the entire read-modify-write atomically via a CAS instruction.

    count++ হলো তিনটি operation — read, add 1, write back। volatile শুধু read ও write-এ happens-before দেয়, কিন্তু দুটি thread একই পুরনো মান read করে একই নতুন মান write করতে পারে — একটি increment হারিয়ে যায়। সমাধান: AtomicInteger.incrementAndGet() — CAS দিয়ে পুরো read-modify-write atomic।

  3. Implement a thread-safe counter using synchronized that two threads each increment 5000 times, then print the final count.
    synchronized দিয়ে একটি thread-safe counter তৈরি করুন। দুটি thread প্রতিটি ৫০০০ বার increment করবে এবং শেষ মান print করবে।
    ✨ Show Answer (উত্তর দেখুন)
    Main.java
    class SafeCounter {
        private int n = 0;
        synchronized void inc() { n++; }
        synchronized int  get() { return n; }
    }
    
    class Main {
        public static void main(String[] args) throws Exception {
            SafeCounter c = new SafeCounter();
            Thread t1 = new Thread(() -> { for(int i=0;i<5000;i++) c.inc(); });
            Thread t2 = new Thread(() -> { for(int i=0;i<5000;i++) c.inc(); });
            t1.start(); t2.start();
            t1.join();  t2.join();
            System.out.println("final = " + c.get()); // always 10000
        }
    }
  4. Write the correct Initialization-on-Demand Holder singleton for a DatabasePool class with a url field, then verify the same instance is returned from two threads.
    DatabasePool class-এর জন্য সঠিক IODH singleton লিখুন। দুটি thread থেকে একই instance ফেরত আসছে কিনা verify করুন।
    ✨ Show Answer (উত্তর দেখুন)
    Main.java
    class DatabasePool {
        final String url;
        private DatabasePool() { url = "jdbc:postgresql://db.abcltech.com/prod"; }
    
        private static class Holder {
            static final DatabasePool INSTANCE = new DatabasePool();
        }
    
        static DatabasePool get() { return Holder.INSTANCE; }
    }
    
    class Main {
        public static void main(String[] args) throws Exception {
            DatabasePool[] results = new DatabasePool[2];
            Thread t1 = new Thread(() -> results[0] = DatabasePool.get());
            Thread t2 = new Thread(() -> results[1] = DatabasePool.get());
            t1.start(); t2.start();
            t1.join();  t2.join();
            System.out.println("same instance: " + (results[0] == results[1]));
            System.out.println("url: " + results[0].url);
        }
    }
  5. In 3 sentences, explain why the IODH pattern does not need volatile or explicit synchronized, yet is still thread-safe.
    তিন বাক্যে বলুন — IODH pattern-এ volatile বা synchronized না থাকলেও এটি কেন thread-safe।
    ✨ Show Answer (উত্তর দেখুন)

    Answer: The JVM initializes a class exactly once, and the class-loading mechanism is inherently thread-safe — the JMM guarantees a happens-before between the static initializer completing and any subsequent read of a static field. The Holder class is not loaded until the first call to get(), giving lazy initialization for free. Because INSTANCE is a final static field set in the static initializer, its value is safely published to all threads without needing any additional synchronization.

    JVM একটি class মাত্র একবার initialize করে, এবং class-loading mechanism inherently thread-safe — JMM static initializer শেষ হওয়া এবং static field read-এর মধ্যে happens-before guarantee দেয়। Holder class প্রথম get() call-এ load হয়, তাই lazy initialization আপনাআপনি পাওয়া যায়। INSTANCE একটি final static field হওয়ায় সব thread-এ নিরাপদে publish হয় — আলাদা synchronization লাগে না।

Summary — Module 41

The Java Memory Model answers the question: when is a write by Thread A guaranteed to be seen by Thread B? The answer is: only when a happens-before relationship exists — established by volatile, synchronized, Thread.start(), Thread.join(), or final field initialization. Without one of these mechanisms you have a data race, and your program can observe stale values, torn writes, or reordered operations. Classic pitfall: broken double-checked locking — fixed by adding volatile or, better, by using the IODH singleton pattern.

Java Memory Model উত্তর দেয় — Thread A-র write Thread B কখন দেখতে পাবে? উত্তর: শুধুমাত্র যখন happens-before সম্পর্ক আছে — volatile, synchronized, Thread.start(), Thread.join(), বা final field initialization-এর মাধ্যমে। এর কোনোটি ছাড়া আপনার কাছে data race আছে — পুরনো মান, torn write বা reorder দেখতে পাবেন। Classic pitfall: ভুল double-checked locking — সমাধান volatile অথবা IODH singleton।

Next Module → Annotations & Reflection — আধুনিক Java framework-এর মেটাডেটা ভিত্তি।