🔥 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
synchronized
block happens-before any subsequent locking of the same block. - Volatile variables: A write to a
volatile
variable 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 onvolatile
for 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
.