How do you detect and quantify lock contention in a running Java application?
answer
- Thread dump: many BLOCKED on same monitor
- JFR JavaMonitorEnter = blocked time + class + stack
- async-profiler -e lock = contention flame graph
- JMH to compare designs under load
- Symptoms: involuntary ctx-switches, CPU low but queue full
- Confirm → quantify → fix top → re-measure
basics
~20 sLook for threads stuck waiting on the same lock. A thread dump shows BLOCKED threads and what monitor they're waiting for. Tools like Java Flight Recorder and async-profiler record lock-contention events, telling you which locks are hot and how long threads wait.
solid answer
~50 sStart with a thread dump (jstack or jcmd Thread.print): many threads in BLOCKED state all waiting to lock the same monitor address is the smoking gun, and the holder is shown so you can see what's slow inside the critical section. For quantified data, use Java Flight Recorder, which emits 'Java Monitor Blocked' / 'Java Monitor Wait' events with the contended object, blocking duration, and stack — open it in JDK Mission Control and sort by total blocked time to find the hottest lock. async-profiler's lock mode produces a flame graph weighted by contention time, which pinpoints the exact call site. JMH microbenchmarks help compare designs (e.g. AtomicLong vs LongAdder) under controlled thread counts. At the OS level, high involuntary context-switch counts and CPU stuck below saturation despite a full queue also hint at contention. The workflow is: confirm with a thread dump, quantify with JFR/async-profiler, then attack the hottest lock.
go deeper
Knows a thread dump exists and that BLOCKED threads indicate waiting on a lock.
Can take and read a thread dump, identify the contended monitor and its holder, and name JFR as a profiling option.
Uses JFR/async-profiler to quantify which lock costs the most blocked time, distinguishes intrinsic-monitor vs ReentrantLock signals, and drives a measure-fix-remeasure loop.
Builds contention observability into the system (continuous JFR, dashboards, SLO-linked tail-latency tracking), interprets OS-level scaling symptoms, and uses JMH to validate design changes before rollout.
## Why you must measure, not guess Contention is invisible in source code — whether a lock is hot depends on runtime access patterns and load. Optimizing the wrong lock wastes effort and adds risk. So the discipline is: **observe first, then act on the proven hot lock.** ## Signal 1 — Thread dumps (free, always available) A **thread dump** is a snapshot of every thread's state and stack. Capture it with `jstack <pid>` or `jcmd <pid> Thread.print` (take 2–3 a few seconds apart). What to look for: - Many threads in state **`BLOCKED`** with a line like `waiting to lock <0x000000076b...>` — and several of them waiting on the **same** monitor address. That's active contention on that lock. - The thread that **owns** that monitor (`locked <0x...076b...>`): its stack shows *what work* is being done under the lock — often the real fix is to make that work shorter or move it out. - `BLOCKED` ≠ `WAITING`/`TIMED_WAITING`: `BLOCKED` means waiting to *enter* a `synchronized`/lock; `WAITING` usually means `Object.wait()`/`park()` (a condition, not raw contention). Limitation: a dump is a single instant — it shows contention only if you happen to catch it. Several dumps showing the same hot monitor is strong evidence. ## Signal 2 — Java Flight Recorder (JFR) — the quantified view JFR is a low-overhead, built-in event recorder. Start it with `jcmd <pid> JFR.start` or `-XX:StartFlightRecording`. Relevant events: - **`jdk.JavaMonitorEnter`** ('Java Monitor Blocked') — fired when a thread blocks entering a monitor; carries the **monitored class**, the **blocking duration**, and a **stack trace**. - **`jdk.JavaMonitorWait`** — `Object.wait()` episodes. - **`jdk.ThreadPark`** — `LockSupport.park` (used by `ReentrantLock`, queues), so it also covers `java.util.concurrent` locks. Open the recording in **JDK Mission Control**, go to the lock/contention view, and **sort by aggregate blocked time** — the top entry is your hottest lock with the exact contended type and call site. This turns 'something is slow' into 'this specific monitor cost 4.2s of blocked time across 50k events.' ## Signal 3 — async-profiler lock mode `async-profiler -e lock` samples contended-lock acquisitions and produces a **flame graph weighted by contention time**. The widest frames are the call sites where threads wait the most — extremely direct for locating the offending code. ## Signal 4 — Benchmarks (JMH) for design comparison When choosing between designs (e.g. `synchronized` counter vs `AtomicLong` vs `LongAdder`, or coarse vs striped), a **JMH** benchmark run at varying thread counts shows how each scales. This is how you *prove* `LongAdder` wins under high write contention before changing production code. ## Signal 5 — OS / JVM symptoms - High **involuntary context switches** (`pidstat -w`, `vmstat`) — threads being parked/unparked fighting over locks. - **CPU below saturation while throughput is flat and a work queue is backing up** — classic 'serialized behind a lock' shape: you have work and cores but can't use them. - Throughput that **drops** as you add threads (negative scaling) is a strong contention tell. ## The workflow 1. **Confirm** with thread dumps (cheap, immediate) — is anything BLOCKED, and on what? 2. **Quantify** with JFR (which lock, how much total blocked time) or async-profiler (which call site). 3. **Fix** the single hottest lock (narrow the section / split / go lock-free). 4. **Re-measure** — contention often just moves to the next lock; iterate. Measure, fix the top offender, re-measure: contention optimization is iterative because relieving one bottleneck exposes the next.
- In a thread dump, how do you tell which thread is causing the contention?Find the monitor address that many BLOCKED threads are 'waiting to lock', then find the thread that has 'locked' that same address — it's the holder. Its stack shows what work it's doing inside the critical section, which is usually the thing to shorten or move out.
- Why might ReentrantLock contention not show up as 'Java Monitor Blocked' in JFR?ReentrantLock isn't a JVM intrinsic monitor; it parks threads via LockSupport. So its contention surfaces as jdk.ThreadPark events (and AbstractQueuedSynchronizer frames in stacks), not jdk.JavaMonitorEnter, which is specific to synchronized/intrinsic monitors.
saying these in an interview costs you the question
- Optimizing a lock by guessing instead of measuring
- Confusing BLOCKED (waiting to enter a lock) with WAITING (wait()/park on a condition)
- Relying on a single thread dump as proof — take several
- Forgetting that ReentrantLock contention shows as ThreadPark, not JavaMonitorEnter