Java Memory Model & happens-before
JMM ও happens-before — thread-safe কোডের পদার্থবিদ্যা
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.
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.
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:
| # | 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। |
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:
volatile ছাড়া flag কাজ নাও করতে পারে।
// 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)");
}
}
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 দেয়।
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 → main memory flush → পরের volatile read সর্বদা fresh মান পায়।
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 নেবে সে সব দেখতে পাবে।
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.
final field constructor শেষ হওয়ার পর সব thread-এর কাছে দৃশ্যমান হয় — কোনো synchronization ছাড়াই। এটি JMM-এর অনেক গুরুত্বপূর্ণ কিন্তু অবহেলিত guarantee।
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();
}
}
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.
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());
}
}
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 পায়।
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();
}
}
volatile at all.
আরো সহজ বিকল্প — Initialization-on-Demand Holder (IODH): inner static class ব্যবহার করুন। Class loading JMM-এর happens-before guarantee দেয়, তাই
volatile-ও লাগে না।
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-এ দৃশ্যমান।
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
| Mechanism | happens-before guarantee | বাংলায় |
|---|---|---|
volatile write → read | Write visible to all subsequent reads of that field. | volatile write সব পরবর্তী read-এ দৃশ্যমান। |
synchronized unlock → lock | All 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 write | Fields visible after constructor exits — no sync needed. | constructor শেষ হলে final field সব thread-এ দৃশ্যমান। |
AtomicX / LockSupport | Same guarantee as volatile. | volatile-এর মতোই guarantee দেয়। |
✅ JMM Do
- Use
volatilefor flags shared between threads - Use
synchronizedfor compound read-modify-write - Prefer immutable objects (
finalfields) for safe sharing - Use
Thread.start()/join()for setup / result transfer - Use IODH or
enumfor thread-safe singletons
⚠️ JMM Avoid
- Bare shared mutable fields without any sync mechanism
volatilefor compound operations (++, compare-and-swap)- Broken DCL (missing
volatileon instance) - Assuming x86 behavior — JMM is weaker than x86
- Sharing an object before its constructor finishes
10. Vocabulary (শব্দভাণ্ডার)
| Term | Meaning | বাংলায় |
|---|---|---|
| JMM | Java Memory Model — rules governing visibility of memory writes across threads. | thread-এর মধ্যে memory write-এর visibility নিয়ন্ত্রণকারী নিয়ম। |
| happens-before | Partial order ensuring that a write is visible to a subsequent read. | write-কে পরের read-এর কাছে দৃশ্যমান করার partial order। |
| Reordering | Compiler/CPU moving memory operations to optimize performance. | performance-এর জন্য compiler/CPU-র instruction ক্রম বদল। |
| volatile | Keyword that establishes happens-before on each write/read of the field. | প্রতিটি write-read-এ happens-before স্থাপন করে। |
| Safe publication | Publishing a reference to another thread such that the object is fully visible. | object-এর reference অন্য thread-এ এমনভাবে দেওয়া যেন পুরো object দৃশ্যমান হয়। |
| DCL | Double-Checked Locking — lazy singleton idiom, requires volatile in Java. | lazy singleton idiom — Java-তে volatile লাগে। |
| IODH | Initialization-on-Demand Holder — safe singleton via inner static class. | inner static class দিয়ে safe lazy singleton। |
| Data race | Two 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.
-
Fix the broken stop-flag program: add
volatileto make the worker thread stop correctly.নিচের broken stop-flag program ঠিক করুন —volatileযোগ করে worker thread সঠিকভাবে থামান।✨ Show Answer (উত্তর দেখুন)
Main.javaclass 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"); } } -
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.volatileguarantees 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, useAtomicInteger.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। -
Implement a thread-safe counter using
synchronizedthat two threads each increment 5000 times, then print the final count.synchronizedদিয়ে একটি thread-safe counter তৈরি করুন। দুটি thread প্রতিটি ৫০০০ বার increment করবে এবং শেষ মান print করবে।✨ Show Answer (উত্তর দেখুন)
Main.javaclass 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 } } -
Write the correct Initialization-on-Demand Holder singleton for a
DatabasePoolclass with aurlfield, then verify the same instance is returned from two threads.DatabasePoolclass-এর জন্য সঠিক IODH singleton লিখুন। দুটি thread থেকে একই instance ফেরত আসছে কিনা verify করুন।✨ Show Answer (উত্তর দেখুন)
Main.javaclass 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); } } -
In 3 sentences, explain why the IODH pattern does not need
volatileor explicitsynchronized, 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
Holderclass is not loaded until the first call toget(), giving lazy initialization for free. BecauseINSTANCEis afinalstatic 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 দেয়।
Holderclass প্রথমget()call-এ load হয়, তাই lazy initialization আপনাআপনি পাওয়া যায়।INSTANCEএকটিfinalstatic 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.
volatile, synchronized, Thread.start(), Thread.join(), বা final field initialization-এর মাধ্যমে। এর কোনোটি ছাড়া আপনার কাছে data race আছে — পুরনো মান, torn write বা reorder দেখতে পাবেন। Classic pitfall: ভুল double-checked locking — সমাধান volatile অথবা IODH singleton।