JVM Concurrency Deep Dive: JMM and Low‑Level Synchronization
This article explains the three fundamental problems of Java concurrency—visibility, atomicity, and ordering—describes how the Java Memory Model and happens‑before rules address them, and details the low‑level implementations and optimizations of volatile, synchronized, lock upgrading, and the removal of biased locking in JDK 15‑17.
After nine previous articles covering memory layout, class loading, bytecode, GC, and JIT, this piece dives into the most intricate part of the JVM: concurrency. It asks why a variable changed by one thread may be invisible to another, why volatile can solve visibility but not guarantee atomicity for i++, and how the lock added by synchronized evolves from near‑zero cost to heavyweight.
1. The three root problems of concurrency
All concurrency bugs can be traced to visibility, atomicity, and ordering. Visibility asks whether a change made by one thread is immediately seen by another; the article illustrates this with a Task class where thread B sets stop = true but thread A may loop forever because it reads a stale copy from its working memory.
Atomicity concerns whether an operation (e.g., i++) executes as an indivisible unit. The article shows that i++ expands to three bytecode steps (load, add, store), so two threads can both read 0, add 1, and store 1, losing one update.
Ordering asks whether the program executes in the written order. Compilers and CPUs may reorder instructions as long as single‑threaded semantics (as‑if‑serial) are preserved, but such reordering can cause bugs in multithreaded code, exemplified by the double‑checked‑locking singleton.
2. Java Memory Model (JMM): main memory and working memory
JMM is an abstract specification that hides hardware and OS memory differences, providing a uniform concurrency semantics across platforms. It defines a main memory that holds all shared variables and a per‑thread working memory that holds a thread’s private copy of those variables. The core rule is that all reads and writes of shared variables must go through the thread’s working memory; direct access to main memory is prohibited.
This explains why the visibility example works: thread B writes stop = true to its working memory, and the timing of flushing to main memory is not guaranteed, so thread A may keep seeing the old false value.
Important reminder: JMM’s "main memory / working memory" is an abstraction, not the same as the JVM’s heap/stack division.
To reason about visibility and ordering without delving into low‑level caches, JMM provides the happens‑before relation.
3. happens‑before: JMM’s promise
If operation A happens‑before operation B, then A’s result is guaranteed to be visible to B and A is logically ordered before B. The relation does not require physical time ordering, only result visibility.
JMM defines several built‑in happens‑before rules, including program order, lock release/acquire, volatile write/read, transitivity, thread start, and thread termination. By following any of these rules, programmers can ensure correct visibility without worrying about underlying reordering.
4. volatile: visibility and ordering
volatileprovides two guarantees: (1) writes are immediately flushed to main memory, and reads always fetch from main memory, ensuring visibility; (2) the JVM inserts memory barriers around volatile reads/writes, preventing certain reorderings and thus preserving ordering.
However, volatile does not make compound actions atomic. The article shows that volatile int count = 0; count++; is still unsafe because count++ remains a read‑modify‑write sequence.
The classic use case is the double‑checked‑locking singleton, where volatile prevents the reordering of object allocation steps (memory allocation → object initialization → reference assignment) that could otherwise expose a partially constructed instance.
5. synchronized and lock upgrading
synchronizedensures mutual exclusion via a monitor associated with each object. Entering a synchronized block executes the monitorenter bytecode; exiting executes monitorexit. The lock state is stored in the object’s Mark Word.
Early implementations used heavyweight OS mutexes, incurring high overhead. HotSpot later introduced lock upgrading with four states: no‑lock, biased lock, lightweight lock, and heavyweight lock. The lock upgrades only when contention increases, following a “pay‑as‑you‑go” strategy.
No‑lock : object is newly created, no thread has locked it.
Biased lock : optimizes the common case where a single thread repeatedly acquires the lock; the thread ID is stored in the Mark Word, allowing lock acquisition without CAS.
Lightweight lock : uses CAS and spin‑waiting; if CAS fails, the thread may spin before upgrading.
Heavyweight lock : falls back to OS mutexes when contention is high, causing thread blocking.
6. Lock optimizations and the demise of biased locking (JDK 8 → 17)
Beyond upgrading, the JIT performs two additional optimizations:
Lock elimination : escape analysis shows an object never escapes the current thread, so the lock is removed.
Lock coarsening : consecutive synchronized blocks on the same lock are merged into a single larger lock region.
Starting with JDK 15 (JEP 374), biased locking is disabled by default and marked deprecated. The article explains two reasons: (1) reduced benefit because modern applications use thread pools and concurrent containers, causing frequent bias revocation; (2) high revocation cost, which requires a safepoint and can cause stop‑the‑world pauses. Consequently, JDK 17 removes biased locking entirely, and lock acquisition starts directly with lightweight locks.
Version note: biased locking is disabled by default in JDK 15 and removed in JDK 17.
Conclusion
Concurrency fundamentals: visibility, atomicity, ordering.
JMM abstracts these via main memory and per‑thread working memory.
happens‑before provides a reasoning framework. volatile guarantees visibility and ordering but not atomicity; classic use case is DCL singleton. synchronized relies on object monitors; lock upgrading follows a cost‑effective path.
JIT lock optimizations (elimination, coarsening) and the removal of biased locking reflect evolving workload characteristics.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
