中文版:管程是怎么实现的:从操作系统内核到 HotSpot 对象监视器
The companion article, From PV Operations and Monitors to Modern Java Concurrency, covers what a monitor is: private data, procedures, implicit mutual exclusion, condition variables, and the Hoare-versus-Mesa signalling question. This one asks the next question — how is it built? — and follows the answer from a single atomic CPU instruction, through Linux wait queues and futexes, up to the object header and ObjectMonitor that sit behind every synchronized block in HotSpot.
Abbreviations
| OS | Operating System |
| CPU | Central Processing Unit |
| CAS | Compare-And-Swap |
| TAS | Test-And-Set |
| LL/SC | Load-Linked / Store-Conditional |
| MCS | Mellor-Crummey and Scott (queue lock) |
| futex | Fast Userspace muTEX |
| NPTL | Native POSIX Thread Library (glibc’s threading implementation) |
| POSIX | Portable Operating System Interface |
| JVM | Java Virtual Machine |
| JDK | Java Development Kit |
| JEP | JDK Enhancement Proposal |
| JUC | java.util.concurrent |
| AQS | AbstractQueuedSynchronizer |
| GC | Garbage Collection |
| IPI | Inter-Processor Interrupt |
0. Three problems every implementation must solve
Whatever layer you look at, a monitor implementation has to answer the same three mechanical questions:
Every design below is one particular answer to these three.
#mermaid-svg-z3HZspTYReemt27E{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-z3HZspTYReemt27E .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-z3HZspTYReemt27E .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-z3HZspTYReemt27E .error-icon{fill:#552222;}#mermaid-svg-z3HZspTYReemt27E .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-z3HZspTYReemt27E .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-z3HZspTYReemt27E .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-z3HZspTYReemt27E .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-z3HZspTYReemt27E .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-z3HZspTYReemt27E .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-z3HZspTYReemt27E .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-z3HZspTYReemt27E .marker{fill:#333333;stroke:#333333;}#mermaid-svg-z3HZspTYReemt27E .marker.cross{stroke:#333333;}#mermaid-svg-z3HZspTYReemt27E svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-z3HZspTYReemt27E p{margin:0;}#mermaid-svg-z3HZspTYReemt27E .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-z3HZspTYReemt27E .cluster-label text{fill:#333;}#mermaid-svg-z3HZspTYReemt27E .cluster-label span{color:#333;}#mermaid-svg-z3HZspTYReemt27E .cluster-label span p{background-color:transparent;}#mermaid-svg-z3HZspTYReemt27E .label text,#mermaid-svg-z3HZspTYReemt27E span{fill:#333;color:#333;}#mermaid-svg-z3HZspTYReemt27E .node rect,#mermaid-svg-z3HZspTYReemt27E .node circle,#mermaid-svg-z3HZspTYReemt27E .node ellipse,#mermaid-svg-z3HZspTYReemt27E .node polygon,#mermaid-svg-z3HZspTYReemt27E .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-z3HZspTYReemt27E .rough-node .label text,#mermaid-svg-z3HZspTYReemt27E .node .label text,#mermaid-svg-z3HZspTYReemt27E .image-shape .label,#mermaid-svg-z3HZspTYReemt27E .icon-shape .label{text-anchor:middle;}#mermaid-svg-z3HZspTYReemt27E .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-z3HZspTYReemt27E .rough-node .label,#mermaid-svg-z3HZspTYReemt27E .node .label,#mermaid-svg-z3HZspTYReemt27E .image-shape .label,#mermaid-svg-z3HZspTYReemt27E .icon-shape .label{text-align:center;}#mermaid-svg-z3HZspTYReemt27E .node.clickable{cursor:pointer;}#mermaid-svg-z3HZspTYReemt27E .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-z3HZspTYReemt27E .arrowheadPath{fill:#333333;}#mermaid-svg-z3HZspTYReemt27E .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-z3HZspTYReemt27E .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-z3HZspTYReemt27E .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-z3HZspTYReemt27E .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-z3HZspTYReemt27E .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-z3HZspTYReemt27E .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-z3HZspTYReemt27E .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-z3HZspTYReemt27E .cluster text{fill:#333;}#mermaid-svg-z3HZspTYReemt27E .cluster span{color:#333;}#mermaid-svg-z3HZspTYReemt27E div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-z3HZspTYReemt27E .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-z3HZspTYReemt27E rect.text{fill:none;stroke-width:0;}#mermaid-svg-z3HZspTYReemt27E .icon-shape,#mermaid-svg-z3HZspTYReemt27E .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-z3HZspTYReemt27E .icon-shape p,#mermaid-svg-z3HZspTYReemt27E .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-z3HZspTYReemt27E .icon-shape .label rect,#mermaid-svg-z3HZspTYReemt27E .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-z3HZspTYReemt27E .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-z3HZspTYReemt27E .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-z3HZspTYReemt27E :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Language: monitor syntaxsynchronized / wait / notify
Runtime: HotSpotmark word + ObjectMonitor
User-space thread library: glibc NPTLpthread_mutex / pthread_cond
Kernel: futex syscallwait queues + scheduler
Hardware: atomic instructionsLOCK CMPXCHG / LL-SC+ memory barriers
This diagram answers: when a synchronized block blocks, which layers does the request pass through?
Part 1: Monitors at the operating-system level
First, a clarification that removes a lot of confusion: a monitor is a language construct, not a kernel object. There is no monitor system call in Linux. What Hoare and Brinch Hansen proposed was a compiler that sees a monitor block, inserts a lock at every procedure entry and an unlock at every exit, and compiles wait/signal into operations on condition variables. So “how does the OS implement monitors?” really means “what primitives does the OS provide so that a compiler or runtime can assemble one?” The answer is two things: locks and wait queues.
1.1 Mutual exclusion starts with one atomic instruction
On a uniprocessor, the only thing that can interrupt a critical section is an interrupt, so disabling interrupts on entry and re-enabling them on exit is enough. That stops working on multiple cores — you only silenced your core.
On a multiprocessor, you need hardware that performs read-modify-write indivisibly:
| TAS / exchange | x86 XCHG | Atomically store a new value, return the old one |
| CAS | x86 LOCK CMPXCHG | Store only if the current value equals the expected one; report success |
| LL/SC | ARM LDXR/STXR (or ARMv8.1 CAS) | Store only if nobody wrote the location since the load |
The LOCK prefix makes the core take exclusive ownership of the cache line in the coherence protocol, so no other core can modify it mid-operation; these instructions also act as memory barriers, so loads and stores inside the critical section cannot be reordered outside it. With that, the simplest lock — a spinlock — is a few lines:
void spin_lock(int *l) { while (xchg(l, 1) == 1) cpu_relax(); } // retry until we win
void spin_unlock(int *l) { store_release(l, 0); }
A spinlock solves mutual exclusion but not blocking: the loser burns CPU the whole time. For hold times of tens of nanoseconds that is actually the fastest option; for long hold times, or when the holder has been descheduled, it is pure waste. So we need the second ingredient.
1.2 Blocking and waking: kernel wait queues and the scheduler
The kernel “stops” a thread by marking it as sleeping, removing it from the run queue and switching to someone else. The canonical Linux pattern for waiting on a condition is:
DEFINE_WAIT(wait);
for (;;) {
prepare_to_wait(&wq, &wait, TASK_INTERRUPTIBLE); // (1) enqueue and mark ourselves asleep
if (condition) // (2) only then test the condition
break;
schedule(); // (3) give up the CPU
}
finish_wait(&wq, &wait);
And the waker:
condition = true;
wake_up(&wq); // mark queued tasks TASK_RUNNING and put them back on a run queue
Why no lost wake-up? Because of the order of (1) and (2). If the waker runs wake_up between (2) and (3), it flips our state back to TASK_RUNNING, so schedule() in (3) sees a runnable task and returns at once. Test the condition before enqueueing and that window would swallow the wake-up.
What suspend and resume physically mean:
- Suspend: schedule() takes the task off the run queue; context_switch() saves registers and switches kernel stacks (and page tables if the next task is in another process). The thread now consumes no CPU at all.
- Resume: try_to_wake_up() sets the task to TASK_RUNNING and puts it on some CPU’s run queue; if that CPU is idle, an IPI (Inter-Processor Interrupt) kicks it.
A wake-up only makes the thread runnable; the scheduler decides when it actually runs. That is the physical origin of Mesa semantics: by the time the woken thread executes, someone else may have changed the condition again, so it must re-check in a while loop.
1.3 The kernel’s own sleeping lock: spin, then queue
The kernel’s struct mutex combines both ingredients into three paths:
| Fast path | Lock is free | One cmpxchg sets owner from 0 to us — done |
| Mid path | Lock is held, but the owner is currently running on a CPU | Optimistic spinning (queued MCS-style) on the bet that it releases soon |
| Slow path | Owner is asleep, or spinning took too long | Join wait_list, schedule(); the unlocker wakes us |
Keep this “CAS first, spin briefly, then queue and sleep” shape in mind. It reappears in futexes, in glibc and in HotSpot.
1.4 Assembling a monitor: the classic Hoare monitor built from semaphores
Given locks and wait queues — which is all a semaphore is — a compiler can translate a monitor mechanically. This is the textbook translation, and it implements Hoare semantics (the signalled thread runs immediately):
semaphore mutex = 1; // monitor entry lock
semaphore next = 0; // signallers that stepped aside sleep here
int next_count = 0; // how many are sleeping on next
for each condition variable x:
semaphore x_sem = 0;
int x_count = 0;
procedure body:
P(mutex);
…body…
if (next_count > 0) V(next); // hand the monitor back to a waiting signaller first
else V(mutex);
x.wait():
x_count++;
if (next_count > 0) V(next); else V(mutex); // leave the monitor
P(x_sem); // sleep on x
x_count–;
x.signal():
if (x_count > 0) {
next_count++;
V(x_sem); // wake one waiter on x and hand it the monitor
P(next); // step aside and sleep on next
next_count–;
}
Reading it makes Hoare’s cost obvious: every signal costs an extra context switch — the signaller sleeps so the signalled thread can run. Mesa semantics shrink x.signal() to “V(x_sem) and keep going”, saving that switch at the price of waiters re-checking their condition. That is the engineering reason both pthreads and Java chose Mesa.
1.5 The user-space shortcut: futex
The kernel machinery above has one drawback: even an uncontended lock would trap into the kernel on every acquire. A system call costs far more than an atomic instruction — typically hundreds of nanoseconds or more, and microseconds once a context switch is involved, versus nanoseconds for an uncontended CAS. The futex (Fast Userspace muTEX, merged in Linux 2.5 in 2002) fixes this by keeping the lock word in user memory and entering the kernel only when someone must actually sleep or be woken.
A futex has two core operations:
- futex_wait(addr, expected) — the kernel, while holding the lock on the hash bucket for addr, checks *addr == expected; only if it still matches does the caller go to sleep. Otherwise it returns EAGAIN immediately.
- futex_wake(addr, n) — wake up to n threads sleeping on addr.
“Check the value” and “go to sleep” are atomic inside the kernel. It is the same anti-lost-wake-up trick as in 1.2, with the condition replaced by a user-space integer.
Ulrich Drepper’s Futexes Are Tricky gives the classic mutex built on it, using three states — 0 free, 1 locked with no waiters, 2 locked with possible waiters:
void lock(int *f) {
int c = cmpxchg(f, 0, 1); // fast path: 0 -> 1, pure user space
if (c != 0) {
if (c != 2) c = xchg(f, 2); // announce "someone is waiting"
while (c != 0) {
futex_wait(f, 2); // slow path: sleep in the kernel
c = xchg(f, 2);
}
}
}
void unlock(int *f) {
if (atomic_dec(f) != 1) { // was 2: there may be waiters
*f = 0;
futex_wake(f, 1); // enter the kernel to wake one
} // was 1: nobody waiting, no syscall
}
Thread B
Kernel futex
User memory f
Thread A
Thread B
Kernel futex
User memory f
Thread A
#mermaid-svg-vs855b2IBAolg4II{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vs855b2IBAolg4II .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vs855b2IBAolg4II .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vs855b2IBAolg4II .error-icon{fill:#552222;}#mermaid-svg-vs855b2IBAolg4II .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vs855b2IBAolg4II .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vs855b2IBAolg4II .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vs855b2IBAolg4II .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vs855b2IBAolg4II .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vs855b2IBAolg4II .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vs855b2IBAolg4II .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vs855b2IBAolg4II .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vs855b2IBAolg4II .marker.cross{stroke:#333333;}#mermaid-svg-vs855b2IBAolg4II svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vs855b2IBAolg4II p{margin:0;}#mermaid-svg-vs855b2IBAolg4II .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vs855b2IBAolg4II text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-vs855b2IBAolg4II .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vs855b2IBAolg4II .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-vs855b2IBAolg4II .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-vs855b2IBAolg4II .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-vs855b2IBAolg4II #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-vs855b2IBAolg4II .sequenceNumber{fill:white;}#mermaid-svg-vs855b2IBAolg4II #sequencenumber{fill:#333;}#mermaid-svg-vs855b2IBAolg4II #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-vs855b2IBAolg4II .messageText{fill:#333;stroke:none;}#mermaid-svg-vs855b2IBAolg4II .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vs855b2IBAolg4II .labelText,#mermaid-svg-vs855b2IBAolg4II .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-vs855b2IBAolg4II .loopText,#mermaid-svg-vs855b2IBAolg4II .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-vs855b2IBAolg4II .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vs855b2IBAolg4II .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vs855b2IBAolg4II .noteText,#mermaid-svg-vs855b2IBAolg4II .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-vs855b2IBAolg4II .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vs855b2IBAolg4II .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vs855b2IBAolg4II .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vs855b2IBAolg4II .actorPopupMenu{position:absolute;}#mermaid-svg-vs855b2IBAolg4II .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-vs855b2IBAolg4II .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vs855b2IBAolg4II .actor-man circle,#mermaid-svg-vs855b2IBAolg4II line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-vs855b2IBAolg4II :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
cmpxchg 0→1 succeeds (no syscall)
cmpxchg 0→1 fails, xchg sets 2
futex_wait(f, 2)
checks *f == 2, queues B, B sleeps
atomic_dec 2→1: waiters exist
*f = 0
futex_wake(f, 1)
B back on a run queue
xchg sets 2, B owns the lock
This diagram answers: at which moments does a futex lock actually enter the kernel?
Bottom line: an uncontended lock and unlock never leave user space; the user/kernel transition happens only when a thread must really sleep or really be woken. That is the physical reason behind the folk wisdom “locks are cheap, contention is expensive.”
1.6 Condition variables: the skeleton of pthread_cond
The hard part of a condition variable is that wait must atomically “release the mutex and sleep”. A simplified futex version:
void cond_wait(cond_t *cv, mutex_t *m) {
int seq = cv->seq; // (1) remember the current signal sequence
mutex_unlock(m); // (2) release the mutex
futex_wait(&cv->seq, seq); // (3) sleep only if no signal arrived since (1)
mutex_lock(m); // (4) compete for the mutex again (Mesa)
}
void cond_signal(cond_t *cv) {
atomic_inc(&cv->seq); // bump the sequence
futex_wake(&cv->seq, 1); // wake one sleeper
}
If a signal lands between (2) and (3), the sequence has already changed and futex_wait returns immediately — nothing is lost. Step (4) is Mesa semantics in code: the woken thread must re-acquire the mutex, so callers must write while (!cond) pthread_cond_wait(…).
Real glibc (the algorithm introduced in 2.25) is considerably more involved: it splits waiters into two groups so a signal can never wake a thread that started waiting after the signal, and it handles sequence wrap-around and cancellation. The four steps above are still its skeleton.
Part 2: Monitors in Java
At the language level every Java object is a monitor. Physically, almost none of them carries one — the memory cost would be prohibitive. HotSpot’s trick is to squeeze lock state into a few bits of the 64-bit object header, and allocate a real ObjectMonitor only when threads genuinely need to queue.
2.1 The bytecode entry points: monitorenter / monitorexit
A synchronized block compiles to a monitorenter / monitorexit pair, and javac also emits an exception handler that runs monitorexit if the block throws — that is “the compiler releases the lock for you”, implemented. A synchronized method gets no such bytecodes; instead its ACC_SYNCHRONIZED flag tells the JVM to lock on invocation and unlock on return. The monitor-theory promise that “locking is inserted by the compiler, so you cannot forget it” is delivered literally.
2.2 The mark word in the object header
On 64-bit HotSpot, the first word of every object header is the mark word. Its two lowest bits (plus, historically, one biased bit) decide what the remaining bits mean. The classic layout, without compact object headers:
| Unlocked | 25 unused · 31 identity hash · 1 unused · 4 GC age | 0 | 01 |
| Biased (removed) | 54 owning-thread pointer · 2 epoch · 1 unused · 4 GC age | 1 | 01 |
| Lightweight (legacy) | 62-bit pointer to an on-stack lock record | — | 00 |
| Heavyweight | 62-bit pointer to an ObjectMonitor | — | 10 |
| GC marked | used by the collector | — | 11 |
Two things to notice:
- Because one word is reused for everything, locking displaces the identity hash and GC age, which then have to be parked elsewhere — in the stack lock record’s “displaced header” (legacy lightweight locking) or inside the ObjectMonitor (heavyweight).
- Compact object headers (experimental in JDK 24 via JEP 450, a product feature in JDK 25 via JEP 519) shrink the header to 64 bits and change this layout; they rely on the newer lightweight locking and the “object monitor table” described below. The table above is the classic version, best for understanding the idea.
2.3 Lock states and the upgrade path
#mermaid-svg-9QFzQjnCZrBgcg1w{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9QFzQjnCZrBgcg1w .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9QFzQjnCZrBgcg1w .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9QFzQjnCZrBgcg1w .error-icon{fill:#552222;}#mermaid-svg-9QFzQjnCZrBgcg1w .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9QFzQjnCZrBgcg1w .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9QFzQjnCZrBgcg1w .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9QFzQjnCZrBgcg1w .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9QFzQjnCZrBgcg1w .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9QFzQjnCZrBgcg1w .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9QFzQjnCZrBgcg1w .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9QFzQjnCZrBgcg1w .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9QFzQjnCZrBgcg1w .marker.cross{stroke:#333333;}#mermaid-svg-9QFzQjnCZrBgcg1w svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9QFzQjnCZrBgcg1w p{margin:0;}#mermaid-svg-9QFzQjnCZrBgcg1w defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-9QFzQjnCZrBgcg1w g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-9QFzQjnCZrBgcg1w g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-9QFzQjnCZrBgcg1w g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-9QFzQjnCZrBgcg1w g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-9QFzQjnCZrBgcg1w g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-9QFzQjnCZrBgcg1w .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-9QFzQjnCZrBgcg1w .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-9QFzQjnCZrBgcg1w .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-9QFzQjnCZrBgcg1w .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9QFzQjnCZrBgcg1w .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-9QFzQjnCZrBgcg1w .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-9QFzQjnCZrBgcg1w .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-9QFzQjnCZrBgcg1w .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9QFzQjnCZrBgcg1w .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9QFzQjnCZrBgcg1w .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9QFzQjnCZrBgcg1w .edgeLabel .label text{fill:#333;}#mermaid-svg-9QFzQjnCZrBgcg1w .label div .edgeLabel{color:#333;}#mermaid-svg-9QFzQjnCZrBgcg1w .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-9QFzQjnCZrBgcg1w .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-9QFzQjnCZrBgcg1w .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-9QFzQjnCZrBgcg1w .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-9QFzQjnCZrBgcg1w .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-9QFzQjnCZrBgcg1w .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9QFzQjnCZrBgcg1w .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9QFzQjnCZrBgcg1w #statediagram-barbEnd{fill:#333333;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9QFzQjnCZrBgcg1w .cluster-label,#mermaid-svg-9QFzQjnCZrBgcg1w .nodeLabel{color:#131300;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-9QFzQjnCZrBgcg1w .note-edge{stroke-dasharray:5;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-note text{fill:black;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram-note .nodeLabel{color:black;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagram .edgeLabel{color:red;}#mermaid-svg-9QFzQjnCZrBgcg1w #dependencyStart,#mermaid-svg-9QFzQjnCZrBgcg1w #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-9QFzQjnCZrBgcg1w .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9QFzQjnCZrBgcg1w :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
CAS on lock bits succeeds
CAS restores on exit
contentionor wait() called
direct contentionor wait() called
idle monitordeflated asynchronously
Unlocked
Lightweight
Heavyweight
This diagram answers: how does an object’s lock state move between unlocked, lightweight and heavyweight? (Biased locking is gone and is not shown.)
Biased locking (historical). The observation behind it: most locks are only ever taken by one thread. The first acquire CASes the thread ID into the mark word; later re-entries by the same thread only compare IDs — no CAS at all. The bill arrives on revocation: when another thread shows up, the owner must be stopped at a safepoint and its stack inspected to see whether it still holds the lock. As uncontended CAS became cheap on modern CPUs, while revocation stayed expensive and the code stayed complex, biased locking was disabled by default and deprecated in JDK 15 (JEP 374), and its flags were obsoleted and the implementation removed in JDK 18. The popular “biased → lightweight → heavyweight” upgrade story describes the default behaviour before JDK 15.
Lightweight locking, legacy (“stack locking”). The thread allocates a lock record in the current stack frame, copies the object’s original mark word into it (the displaced header), then CASes the mark word to point at that record with lock bits 00. A recursive acquire just pushes another lock record with an empty displaced header. Unlock CASes the displaced header back. Only user-space CAS, never a system call.
Lightweight locking, new (lock-stack). Introduced in JDK 21 and the default from JDK 23 (LockingMode defaults to lightweight). Each thread owns a small array, the lock-stack. Locking CASes only the mark word’s lock bits from 01 to 00 and pushes the object reference onto the thread’s lock-stack; the hash and age bits stay where they are, and there is no pointer into anyone’s stack. “Do I own this?” becomes a scan of your own lock-stack. Part of the motivation is to make room for compact object headers and virtual threads, both of which are awkward when the header points into a specific thread’s stack.
Inflation to heavyweight. Triggered when:
- the lightweight CAS fails (contention);
- the owner calls wait() — lightweight locks have no wait set, so a real ObjectMonitor is needed;
- the lock-stack is full, or another implementation limit is hit.
Inflation allocates (or reuses) an ObjectMonitor and points the mark word at it with lock bits 10. From JDK 24, -XX:+UseObjectMonitorTable can instead keep the object-to-monitor mapping in a side hash table so the mark word is not overwritten — a capability compact object headers require.
Do locks ever downgrade? The common claim is “locks only upgrade, never downgrade.” Within a single holding period that is roughly true, but an ObjectMonitor is not welded to its object: since JDK 15 a dedicated thread deflates idle monitors asynchronously, returning the object to the unlocked state and recycling the monitor. The precise statement is: no downgrade while held; deflation once idle.
2.4 ObjectMonitor: the monitor in full
ObjectMonitor is a C++ object — the textbook monitor made concrete inside HotSpot. Key fields in the classic version (roughly JDK 8 through 21):
| _owner | Current owner | The one thread inside |
| _recursions | Re-entry count | Reentrancy |
| _cxq | Singly linked list of new contenders, pushed by CAS (LIFO) | Entry queue (lock-free part) |
| _EntryList | Doubly linked list from which the releasing thread picks a successor | Entry queue |
| _WaitSet | Circular doubly linked list of threads in wait() | Condition-variable queue |
| _succ | Thread already chosen as the presumed successor | Avoids redundant wake-ups |
Newer JDKs have refactored these structures — for example, JEP 491 changed how the owner is recorded (a thread ID rather than a platform-thread pointer) so virtual threads can own monitors, and the entry queues have been consolidated — so field names may differ. The shape — owner, recursion count, entry queue, wait set — has not changed.
Acquire (enter) is the familiar three-stage pattern again:
Release (exit): if _recursions is non-zero, decrement and return. Otherwise release the owner (with a memory barrier). With no waiters, return — no system call on this path either. With waiters, pick a successor from _EntryList (draining _cxq into it first if it is empty) and unpark just that one, avoiding a thundering herd.
Notice that step 3 wakes up to try the CAS again, not to receive the lock. The releasing thread only wakes a successor; the lock itself is left free for anyone to grab, and a newly arriving thread may CAS it before the successor is even scheduled (barging). That buys synchronized throughput, but it means synchronized is not fair — if you need FIFO, use new ReentrantLock(true).
2.5 What wait / notify do inside ObjectMonitor
Thread T2
ObjectMonitor
Thread T1
Thread T2
ObjectMonitor
Thread T1
#mermaid-svg-wMcocsmERtPIF1tm{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wMcocsmERtPIF1tm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wMcocsmERtPIF1tm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wMcocsmERtPIF1tm .error-icon{fill:#552222;}#mermaid-svg-wMcocsmERtPIF1tm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wMcocsmERtPIF1tm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wMcocsmERtPIF1tm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wMcocsmERtPIF1tm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wMcocsmERtPIF1tm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wMcocsmERtPIF1tm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wMcocsmERtPIF1tm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wMcocsmERtPIF1tm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wMcocsmERtPIF1tm .marker.cross{stroke:#333333;}#mermaid-svg-wMcocsmERtPIF1tm svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wMcocsmERtPIF1tm p{margin:0;}#mermaid-svg-wMcocsmERtPIF1tm .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wMcocsmERtPIF1tm text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-wMcocsmERtPIF1tm .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wMcocsmERtPIF1tm .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-wMcocsmERtPIF1tm .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-wMcocsmERtPIF1tm .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-wMcocsmERtPIF1tm #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-wMcocsmERtPIF1tm .sequenceNumber{fill:white;}#mermaid-svg-wMcocsmERtPIF1tm #sequencenumber{fill:#333;}#mermaid-svg-wMcocsmERtPIF1tm #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-wMcocsmERtPIF1tm .messageText{fill:#333;stroke:none;}#mermaid-svg-wMcocsmERtPIF1tm .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wMcocsmERtPIF1tm .labelText,#mermaid-svg-wMcocsmERtPIF1tm .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-wMcocsmERtPIF1tm .loopText,#mermaid-svg-wMcocsmERtPIF1tm .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-wMcocsmERtPIF1tm .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wMcocsmERtPIF1tm .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-wMcocsmERtPIF1tm .noteText,#mermaid-svg-wMcocsmERtPIF1tm .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-wMcocsmERtPIF1tm .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wMcocsmERtPIF1tm .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wMcocsmERtPIF1tm .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wMcocsmERtPIF1tm .actorPopupMenu{position:absolute;}#mermaid-svg-wMcocsmERtPIF1tm .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-wMcocsmERtPIF1tm .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wMcocsmERtPIF1tm .actor-man circle,#mermaid-svg-wMcocsmERtPIF1tm line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-wMcocsmERtPIF1tm :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
holds lock, calls wait()
T1 joins _WaitSet
saves recursions, fully releases
parks
acquires lock, changes state
notify()
T1 moved from _WaitSet
to entry queue (still asleep)
leaves synchronized, releases
T1 chosen as successor, unparked
re-acquires lock, restores recursions
re-checks condition in while loop
This diagram answers: which ObjectMonitor queues does a thread move through during wait/notify, and when is it actually woken?
Facts you can read straight off the implementation:
- wait() requires ownership. Its first step checks that _owner is the caller and throws IllegalMonitorStateException otherwise.
- wait() releases all re-entries. It saves _recursions, zeroes it, exits completely, and restores the count after re-acquiring — which is why calling wait() three synchronized levels deep does not deadlock.
- notify() wakes nobody immediately. It only moves a waiter from _WaitSet to the entry queue. The notified thread is unparked later, once the notifier has left the critical section and the waiter has been chosen as successor. That is Mesa’s signal-and-continue, visible in the source.
- There is only one _WaitSet, so threads waiting for different conditions share it — hence the default advice to use notifyAll(). For several conditions, use ReentrantLock with multiple Conditions; each has its own wait queue.
2.6 How the JVM reaches the OS primitives
HotSpot does not issue futex system calls directly; it goes through a platform layer:
| synchronized contention / wait() | ObjectMonitor → per-thread ParkEvent → os::PlatformEvent::park() | pthread_mutex + pthread_cond_wait |
| LockSupport.park() (AQS, ReentrantLock, Condition) | per-thread Parker | pthread_mutex + pthread_cond_(timed)wait |
| Both | glibc NPTL | futex(FUTEX_WAIT / FUTEX_WAKE) system calls |
PlatformEvent carries a small counter as its own fast path: if the unpark arrives before the park (the permit is already available), park returns without touching pthreads at all. Only a real sleep calls pthread_cond_wait, which glibc turns into futex_wait, which lands in the kernel wait queue and scheduler from Part 1. End to end:
synchronized contention
-> CAS fails -> spinning fails -> enqueue on ObjectMonitor
-> ParkEvent::park -> pthread_cond_wait
-> futex(FUTEX_WAIT) syscall <- first entry into kernel mode
-> kernel: join hash-bucket queue, schedule() away
…
owner exits -> unpark successor -> pthread_cond_signal
-> futex(FUTEX_WAKE) <- the waker enters kernel mode
-> try_to_wake_up() -> successor back on a run queue
On macOS, pthreads sit on Darwin’s psynch system calls rather than futexes, but the structure is the same.
2.7 Virtual threads: moving “sleep” from the kernel into the JVM
A virtual thread (JEP 444, JDK 21) that blocks does not take the path to futex above. It saves its frames as a continuation on the heap and unmounts from its carrier platform thread, which goes off to run other virtual threads. To resume, the JVM’s scheduler (a ForkJoinPool) mounts it again on some carrier.
In JDK 21–23, blocking inside synchronized pinned the carrier — partly because the old implementation tied lock ownership to a particular platform thread and its stack. JEP 491 (JDK 24) reworked how ObjectMonitor records its owner, so synchronized now unmounts in most cases. Pinning still happens when there is a native frame on the stack and during class initialisation.
Part 3: The two layers side by side
| Atomicity of mutual exclusion | Hardware atomics (LOCK CMPXCHG, LL/SC); interrupt masking on a uniprocessor | The same CAS, applied to the mark word or _owner |
| Uncontended fast path | futex lock: one user-space CAS, zero syscalls | Lightweight lock: one CAS (biased locking used to skip even that) |
| Brief contention | Kernel mutex optimistic spinning; glibc adaptive mutexes optional | ObjectMonitor adaptive spinning |
| Entry queue | Kernel wait_list / futex hash-bucket queue | _cxq + _EntryList |
| Condition variable | pthread_cond: sequence counter + futex | _WaitSet (only one per object) |
| Lost wake-up prevention | Enqueue or record a sequence first, test second, sleep last — checked atomically in the kernel | Join _WaitSet before releasing the lock; ParkEvent permit counter |
| Suspend | Off the run queue + context switch | park → pthread_cond_wait → futex |
| Resume | Back on a run queue, IPI if needed | unpark one successor → futex wake |
| Signalling semantics | Mesa (pthreads) | Mesa |
| Fairness | Generally not guaranteed | Not guaranteed; barging allowed |
| When kernel mode is entered | Only to really sleep or really wake | Only after inflation, on a real park / unpark |
Part 4: What this adds to the companion article
| Monitor definition, Hoare vs. Mesa | Covered | Adds the implementation cost of each (1.4) |
| synchronized / wait / notify usage rules | Covered | Adds where each rule comes from inside ObjectMonitor (2.5) |
| AQS, Condition, Semaphore | Covered | Only the shared park path to the OS (2.6) |
| Atomic instructions, spinlocks, kernel wait queues, lost wake-up | Not covered | 1.1–1.3 |
| futex fast/slow paths, pthread_cond | Named only | 1.5–1.6 |
| Mark word, lock upgrades, inflation and deflation | One sentence | 2.2–2.3 |
| ObjectMonitor structure, barging and fairness | Not covered | 2.4 |
| Virtual threads | Covered (unmounting and pinning) | Adds why pinning happened (2.7) |
Two simplifications in the companion article have been corrected there:
Conclusions
- A monitor is not a kernel object; it is a language construct assembled from locks and wait queues. The OS supplies atomic instructions, futexes, wait queues and a scheduler; the compiler and runtime turn them into implicit mutual exclusion plus condition variables.
- Every layer uses the same three stages: CAS first (pure user space), spin briefly (bet the owner releases soon), then queue and sleep (enter the kernel). The kernel mutex, futex locks, glibc and HotSpot’s ObjectMonitor all follow it.
- There is one core trick against lost wake-ups: register your intent to wait (enqueue or record a sequence number) before testing the condition, then sleep — with the test-and-sleep done atomically in the kernel.
- User/kernel transitions happen only under real contention. An uncontended synchronized is one CAS; only an inflated monitor that must actually park or unpark reaches the futex syscall via pthread_cond.
- Java’s monitors materialise on demand. Normally a monitor is a couple of bits in the mark word; it inflates into an ObjectMonitor only for queueing or wait(), and is deflated asynchronously once idle. Biased locking is history, and since JDK 23 lightweight locking itself uses the lock-stack design.
- Mesa semantics have a physical cause: a kernel wake-up only makes a thread runnable, and notify() only moves a thread back to the entry queue. By the time it runs, the condition may have changed — so the while loop stays.
References
- C. A. R. Hoare, “Monitors: An Operating System Structuring Concept”, Communications of the ACM, 1974 (including the semaphore-based construction).
- B. Lampson, D. Redell, “Experience with Processes and Monitors in Mesa”, Communications of the ACM, 1980.
- A. Silberschatz et al., Operating System Concepts, sections “Monitors” and “Implementing a Monitor Using Semaphores”.
- H. Franke, R. Russell, M. Kirkwood, “Fuss, Futexes and Furwocks: Fast Userlevel Locking in Linux”, Ottawa Linux Symposium, 2002.
- U. Drepper, “Futexes Are Tricky”, 2011.
- Linux kernel documentation: Documentation/locking/mutex-design.rst; man 2 futex.
- OpenJDK HotSpot sources: src/hotspot/share/runtime/objectMonitor.{hpp,cpp}, synchronizer.cpp, lockStack.hpp, os/posix/os_posix.cpp (PlatformEvent, Parker).
- JEP 374 (Deprecate and Disable Biased Locking), JEP 444 (Virtual Threads), JEP 450 / JEP 519 (Compact Object Headers), JEP 491 (Synchronize Virtual Threads without Pinning).
- D. Dice, “Biased Locking in HotSpot”; OpenJDK Wiki, “Synchronization”.
- Companion article: From PV Operations and Monitors to Modern Java Concurrency.
网硕互联帮助中心





评论前必须登录!
注册