🔥 What is the Java Memory Model (JMM)?
The Java Memory Model defines how threads interact through memory and what rules the JVM follows when reading/writing shared variables across threads.
🔧 Why is JMM Needed?
In a multi-threaded environment, different threads can run on different CPU cores, with each core having its own cache. Without a clear memory model, we could have:
- Threads seeing stale values.
- Reordered operations.
- Some writes by one thread becoming invisible to others.
The JMM ensures consistent visibility, ordering, and atomicity guarantees — so developers can reason about multi-threaded code reliably.
💡 Key Concepts in JMM
1️⃣ **Happens-Before Relationship
This is the core rule:**
If action A “happens-before” action B, then A’s result is visible to B.
If there’s no happens-before relation, there are no guarantees — B might see an old/stale value written by A, or even no write at all.
Examples of Happens-Before Rules
- Thread start:
Thread.start()happens-before any code inside the thread. - Thread join: Actions inside a thread happen-before
Thread.join()completes. - Locking: Unlocking a
synchronizedblock happens-before any subsequent locking of the same block. - Volatile variables: A write to a
volatilevariable happens-before any subsequent read of that variable by any thread.
2️⃣ Volatile Variables
A variable declared volatile has:
✅ Visibility Guarantee: When one thread writes to it, all other threads immediately see the updated value.
❌ No Atomicity Guarantee: Compound actions (like x++) are still not atomic even if x is volatile.
3️⃣ Synchronization (Locks)
Using synchronized ensures: ✅ Visibility: All changes to variables made inside the synchronized block are visible to other threads entering synchronized blocks protected by the same lock. ✅ Mutual Exclusion: Only one thread can enter the block at a time.
4️⃣ Reordering & Compiler Optimizations
The JVM, JIT compiler, and hardware are free to reorder instructions for performance, as long as it does not break the happens-before rules.
Example:
int a = 1;
int b = 2;
These can be swapped if the compiler thinks it’s more efficient — unless there’s a happens-before reason that prevents it.
5️⃣ Atomicity Guarantees
The JMM guarantees atomicity for: ✅ volatile reads/writes (but not compound operations like ++)
✅ final fields during construction (once fully constructed, final fields are guaranteed to be visible correctly to all threads)
📊 Summary Table
| Guarantee | Volatile | Synchronized |
|---|---|---|
| Visibility | ✅ | ✅ |
| Atomicity | ❌ (except single writes/reads) | ✅ |
| Mutual Exclusion | ❌ | ✅ |
| Prevent Reordering | ✅ | ✅ |
⚠️ Common Example (Without JMM Guarantees)
class Counter {
private int count = 0;
public void increment() {
count++; // Not thread-safe
}
public int getCount() {
return count; // May see stale value
}
}
Without proper synchronization (like synchronized or volatile), another thread could see stale values or see broken intermediate states.
🚀 Example with Proper JMM Guarantees
class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Atomic and safe
}
public int getCount() {
return count.get(); // Guaranteed fresh
}
}
or with synchronized:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
🔚 Final Thought
The JMM is all about:
✅ Visibility (Do other threads see the latest values?)
✅ Ordering (Are instructions executed in a predictable order?)
✅ Atomicity (Are operations fully completed without interruption?)
Here’s a clear comparison table for synchronized, ReentrantLock, and volatile based on the two key parameters: visibility and atomicity.
| Mechanism | Visibility | Atomicity |
|---|---|---|
| synchronized | ✅ Ensures visibility (flushes changes to main memory when lock is released) | ✅ Ensures atomicity (entire block is mutually exclusive) |
| ReentrantLock | ✅ Ensures visibility (same as synchronized) | ✅ Ensures atomicity (lock guarantees mutual exclusion) |
| volatile | ✅ Ensures visibility (reads always from main memory, writes immediately visible) | ❌ No atomicity (only single reads/writes are atomic, compound actions like x++ are not safe) |
🔔 Explanation
✅ Visibility
synchronized: Changes inside a synchronized block are flushed to main memory when the lock is released. Other threads see the latest value when they acquire the same lock.ReentrantLock: Works the same way assynchronized, ensuring visibility when acquiring and releasing the lock.volatile: Ensures every read happens directly from main memory, and every write is immediately flushed to main memory.
✅ Atomicity
synchronized: Ensures atomicity because only one thread can execute the block at a time.ReentrantLock: Works the same — only the thread holding the lock can proceed, so operations inside are atomic.volatile: ❌ Does not ensure atomicity for compound actions (likex++orlist.add()), because these are read-modify-write sequences that can be interrupted between steps.
💡 Quick Rule of Thumb
| When to Use | Recommended Mechanism |
|---|---|
| Just need visibility (read/write shared flag) | volatile |
| Need visibility + atomicity (updating counters, complex operations) | synchronized or ReentrantLock |
Let’s extend the table to include AtomicInteger, so you have a complete view of how synchronized, ReentrantLock, volatile, and AtomicInteger compare in terms of visibility and atomicity.
| Mechanism | Visibility | Atomicity |
|---|---|---|
| synchronized | ✅ Ensures visibility | ✅ Ensures atomicity (full mutual exclusion) |
| ReentrantLock | ✅ Ensures visibility | ✅ Ensures atomicity (full mutual exclusion) |
| volatile | ✅ Ensures visibility | ❌ No atomicity (only single reads/writes are atomic) |
| AtomicInteger | ✅ Ensures visibility (internally uses volatile) | ✅ Ensures atomicity (provides atomic methods like incrementAndGet()) |
🔔 Explanation
✅ Visibility
synchronized: Ensures changes inside the block are visible to all threads when the lock is released.ReentrantLock: Same — visibility is guaranteed when acquiring and releasing the lock.volatile: Every read happens directly from main memory; every write is flushed to main memory immediately.AtomicInteger: Internally relies onvolatilefor visibility and ensures visibility across threads.
✅ Atomicity
synchronized: Full mutual exclusion guarantees atomicity for everything inside the block.ReentrantLock: Same assynchronized— atomicity guaranteed by mutual exclusion.volatile: ❌ Only single reads/writes are atomic. Compound operations (likex++) are not atomic.AtomicInteger: ✅ All operations (likeincrementAndGet(),compareAndSet()) are atomic using low-level compare-and-swap (CAS) operations.
🔧 Quick Recommendations
| Situation | Recommended |
|---|---|
| Simple visibility guarantee (read/write a flag) | volatile |
| Incrementing a counter across threads | AtomicInteger |
| Updating multiple variables together (e.g., transfer balance between accounts) | synchronized or ReentrantLock |
| Need advanced locking (tryLock, fairness, condition variables) | ReentrantLock |
🚀 Key Takeaway
If you want a lightweight way to safely update a single counter, AtomicInteger is perfect.
For more complex critical sections (multiple operations, multiple variables), go for synchronized or ReentrantLock.