Java.Core.JMM

🔥 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

GuaranteeVolatileSynchronized
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.

MechanismVisibilityAtomicity
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 as synchronized, 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 (like x++ or list.add()), because these are read-modify-write sequences that can be interrupted between steps.

💡 Quick Rule of Thumb

When to UseRecommended 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.


MechanismVisibilityAtomicity
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 on volatile for visibility and ensures visibility across threads.

✅ Atomicity

  • synchronized: Full mutual exclusion guarantees atomicity for everything inside the block.
  • ReentrantLock: Same as synchronized — atomicity guaranteed by mutual exclusion.
  • volatile: ❌ Only single reads/writes are atomic. Compound operations (like x++) are not atomic.
  • AtomicInteger: ✅ All operations (like incrementAndGet(), compareAndSet()) are atomic using low-level compare-and-swap (CAS) operations.

🔧 Quick Recommendations

SituationRecommended
Simple visibility guarantee (read/write a flag)volatile
Incrementing a counter across threadsAtomicInteger
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.

This entry was posted in Без рубрики. Bookmark the permalink.