You add synchronized around a short update because two requests can modify the same account at once:
synchronized (account) {
account.update();
}
The Java rule is clear: one thread at a time owns that monitor, and the monitor is released when the region finishes, including when it exits by throwing an exception. The implementation question is less obvious: does every entry allocate a heavyweight monitor and ask the operating system to block?
On JDK 25 HotSpot, usually not. HotSpot uses a lightweight locking path for the common uncontended case and can move to an ObjectMonitor when it needs richer state. These are runtime representations of the same Java monitor semantics.
synchronized is the Java/JVM monitor abstraction; an ObjectMonitor is one HotSpot representation of that abstraction.
What synchronization means to the JVM
A synchronized block and a synchronized method have the same monitor semantics, but the class file represents them differently.
For a block, the compiler emits monitor operations around the protected bytecode:
synchronized (account) {
account.update();
}
monitorenter account
protected instructions
monitorexit account
The compiler also arranges for the monitor to be released on exceptional exits. At the JVM level these are the monitorenter and monitorexit instructions.
A synchronized method is marked with the ACC_SYNCHRONIZED method flag. The invocation machinery acquires the monitor before the body runs and releases it when the invocation completes. An instance method uses the receiver (this); a static method uses the Class object for the declaring class. It does not normally need explicit monitorenter and monitorexit instructions in the method body.
Both forms participate in Java monitor semantics, including mutual exclusion, visibility across unlock/lock, and reentrancy. The monitor is tied to the chosen object, so unrelated code that does not use the same monitor is not excluded from accessing that object’s fields.
Why the uncontended path matters
When one thread enters and leaves a small synchronized region without another thread trying to acquire the same monitor, queueing and sleeping would add work without helping anyone make progress. HotSpot therefore tries a short fast path for this common case.
The object header participates in that path. An ordinary HotSpot object has a Mark Word, a machine-word-sized header field that also carries information such as identity-hash and garbage-collector state. In the JDK 25 lightweight-locking model, low lock-state bits distinguish a neutral object, a fast-locked object, and an object whose lock state is represented by a monitor. Exact header layout has configuration and implementation details; this diagram shows only the lock-state role.
Older HotSpot diagrams often show the Mark Word being replaced with a pointer to a BasicLock record on a thread’s stack. That describes the earlier stack-locking representation. It is not the right default picture for JDK 25 lightweight locking: the normal path keeps lock-state bits in the Mark Word and ownership bookkeeping in a HotSpot LockStack associated with the executing Java thread.
On an uncontended entry, HotSpot examines the object’s lock state and attempts an atomic transition to the fast-locked state. The owning thread records the object in its LockStack as part of the protocol. The atomic transition arbitrates between threads that race to acquire the same neutral object; only one can establish fast ownership at a time. The exact instruction sequence and bookkeeping order depend on the runtime and processor.
The fast path still preserves Java’s reentrancy rule. If a thread already owns a monitor, it may enter it again, and each matching exit reverses one acquisition. HotSpot’s lightweight and inflated representations must preserve that rule, but the implementation can account for recursive ownership differently across representations. You should reason from the Java rule rather than assume one particular recursion counter layout.
When HotSpot needs an ObjectMonitor
Now let Thread A hold account while Thread B tries to enter. B cannot run the protected region yet. HotSpot has more work to coordinate: ownership, recursive entry, entrants, threads that have called wait(), and parking or waking threads that cannot proceed.
HotSpot can inflate the lock, moving its representation to an ObjectMonitor. The object’s Mark Word then indicates the monitor representation. An ObjectMonitor provides richer runtime state, including an owner, recursion bookkeeping, contender/entry structures, and a wait set. HotSpot can spin briefly in some situations and may park a thread when waiting is more appropriate; it uses runtime and operating-system support to park and wake threads.
Inflation is a representation transition, not a synonym for “use an OS mutex.” HotSpot’s monitor code manages ownership and queues in the JVM, may spin, and can use park/unpark machinery when threads need to sleep. The kernel scheduler becomes relevant when a platform thread is parked, but it is not the definition of an inflated Java monitor.
Also, “contended” does not mean every collision must immediately inflate. HotSpot has fast and slow paths, and details vary by configuration and release. Inflation is available when the runtime needs monitor state that is not conveniently represented by the lightweight path.
Entry contention is different from wait()
Two states are easy to confuse because both can leave a thread unable to run inside a synchronized region.
If Thread B reaches monitorenter while Thread A owns the monitor, B is an entry contender. B has not acquired the monitor and cannot execute the critical section.
Object.wait() has a different contract. The calling thread must already own the object’s monitor. Calling wait() releases that monitor and places the caller in the object’s wait set. After notification, timeout, or interruption makes it eligible to continue, the thread must reacquire the same monitor before wait() returns. Notification does not transfer ownership directly to the notified thread.
wait() releases the monitor while the thread waits, then reacquires it before returning; notification only makes the waiter eligible to compete again.
The JLS defines a wait set for each object monitor. In HotSpot, wait() needs that richer wait-set state, so it can cause the lightweight representation to inflate even when a long queue of entrants was not what prompted the transition. This is why a monitor can inflate because an operation needs monitor facilities, not only because a benchmark created heavy contention.
The representation can change back
Inflation does not mean “heavyweight forever.” Once an inflated monitor is inactive and eligible for reclamation, HotSpot can deflate it and reclaim the runtime monitor structure. Deflation must coordinate safely with threads that may still be entering, exiting, waiting, or observing the object, so reclamation is a managed runtime operation rather than an immediate reversal at the end of a critical section.
Think of the lock representation as having a lifecycle: a neutral or lightweight state can become inflated when richer state is needed, and an idle inflated monitor can later be eligible for deflation.
Why this connects to virtual threads
Lesson 19 separated a virtual thread’s Java identity from the carrier that happens to execute it. That distinction matters here because lightweight lock ownership is associated with a HotSpot thread’s LockStack, while a virtual thread can unmount from one carrier and later resume on another.
JEP 491, delivered in JDK 24 and included in JDK 25, changed HotSpot so ordinary intrinsic synchronization no longer inherently pins a virtual thread to its carrier. As a virtual thread freezes, HotSpot moves its lightweight lock-stack object references into continuation stack-chunk state and restores them when it mounts again. Inflated monitor ownership also has to follow the logical Java-thread identity, rather than treating one carrier as the permanent owner.
This changes the carrier story, not the lock’s mutual-exclusion rule. If a virtual thread blocks while holding a monitor, the monitor remains owned and can still limit other work that needs it. Releasing the carrier does not release the monitor.
Diagnose contention from evidence
The useful performance question is not “does this code use synchronized?” Uncontended intrinsic synchronization is designed to be cheap. The costs to investigate are contention and its consequences: queueing, spinning, parking, wakeups, cache-coherence traffic on shared state, and the amount of useful work protected by the critical section.
A production investigation should connect evidence to the question being asked:
- Capture repeated thread dumps during the slow interval, for example with
jcmd <pid> Thread.print -l. Look for threads blocked entering monitors, the monitor owner, and repeated stacks pointing to the same lock-protected path. ABLOCKEDthread is waiting to enter; a thread inObject.wait()has released the monitor and waits for notification or another wake condition. One dump is a snapshot, not a contention rate. - Use Java Flight Recorder monitor events or platform monitoring APIs to determine whether monitor-enter waits recur and how long they last. Event thresholds and recording settings affect what is recorded, so absence of an event is not proof that no synchronization cost exists.
- Correlate contention with request latency, throughput, CPU, and the protected operation. A long wait can point to an oversized critical section or a slow call made while holding a lock; a hot lock with many short waits can indicate shared-state serialization.
- Change lock scope or coordination only after you understand the invariant being protected. Replacing
synchronizedwith another lock does not remove serialization on the same shared resource.
An inflated monitor in a dump or event is useful evidence about representation or contention, but it does not by itself prove that the monitor is the application’s dominant cost. Workload, lock frequency, queue length, and critical-section duration determine the impact.
Check your reasoning
Why does a thread in `wait()` not immediately resume inside the synchronized block after another thread calls `notify()`?
Notification makes the waiter eligible to continue, but it does not hand the monitor to that thread. The waiter must compete to reacquire the monitor, and it can return from wait() only after reacquiring it. Another thread may acquire the monitor first, so condition checks belong in a loop.
A virtual thread blocks in a JDK 25 synchronized region and unmounts. Can another thread enter the same monitor because the carrier was released?
No. Unmounting releases the carrier execution resource; it does not release the monitor. The virtual thread remains the logical owner until it exits or waits on that monitor, so contenders still cannot enter.
A profiler reports monitor inflation. What can you conclude?
HotSpot used an ObjectMonitor representation for the monitor. Inflation can follow contention or a need for richer monitor operations such as wait(); the observation alone does not establish a throughput problem. Correlate repeated wait evidence with the application's slow path.
The complete path
For JDK 25 HotSpot, remember the division of responsibilities: Java defines monitor behavior; the Mark Word carries a lock-state encoding; the LockStack tracks lightweight ownership; and an ObjectMonitor supplies richer state for operations that need it. Virtual-thread lock state follows the virtual thread across carrier changes, while monitor ownership continues to enforce mutual exclusion.
The next lesson turns to explicit synchronizers: ReentrantLock, AQS, and LockSupport.park(). That mechanism uses Java-level queueing and parking to build lock behavior, which gives us a useful comparison with HotSpot’s intrinsic-monitor machinery.
Sources and implementation boundaries
Based on Daily JVM Byte #20 and its reviewed draft requirements. Diagrams and fast-path sequences are explanatory, not captured execution traces; no benchmark or forced inflation experiment was run. The Java monitor contract follows the Java SE 25 specifications. Header states, LockStack bookkeeping, inflation, deflation, and continuation integration describe HotSpot on JDK 25 and may vary by release, build, platform, or configuration. The exact low-bit illustration is the standard JDK 25 HotSpot locking encoding, not a Java guarantee.
- Java Language Specification, synchronized statements
- Java Language Specification, synchronized methods
- Java Virtual Machine Specification, method invocation synchronization
- Java Virtual Machine Specification,
monitorenterandmonitorexit - JEP 491: Synchronize Virtual Threads without Pinning
- OpenJDK 25 update source, Mark Word lock-state encodings
- OpenJDK 25 update source, lightweight locking and monitor inflation/deflation
- OpenJDK 25 update source, ObjectMonitor state
- Java SE 25 ThreadInfo management API
- Java SE 25 MonitorInfo management API
- OpenJDK 25u JFR event metadata, including JavaMonitorEnter