Thread stuck in BLOCKED state
Scenario
Thread dumps show many threads in BLOCKED waiting on the same monitor. Requests time out or throughput collapses. You need to know which lock, who holds it, and why they do not release it—without guessing from class names alone.
After reading, you should be able to:
- Distinguish
BLOCKEDvsWAITINGvsTIMED_WAITING. - Read a
jstackline: “waiting to lock <0x…>” and find the owner thread. - Identify lock contention vs pool exhaustion vs external I/O.
- Fix with smaller critical sections and better concurrency primitives.
Why — BLOCKED means waiting for a Java monitor
In a thread dump, BLOCKED (on java.lang.Thread.State) means the thread is trying to enter a
synchronized block or method but another thread already holds that monitor.
It is not sleeping on I/O—that is usually RUNNABLE in native code or blocked in socket read depending on JVM reporting.
Thread states (quick reference)
| State | Meaning | Typical cause |
|---|---|---|
| BLOCKED | Waiting for monitor lock | synchronized, contended intrinsic lock |
| WAITING | Waiting indefinitely | Object.wait(), LockSupport.park, join |
| TIMED_WAITING | Waiting with timeout | sleep, wait(timeout), pool get(timeout) |
| RUNNABLE | Executing or runnable | On CPU, or in JNI I/O (can look “stuck”) |
java.util.concurrent.locks.ReentrantLock contention often shows as WAITING on AbstractQueuedSynchronizer, not BLOCKED—still a lock problem.
Why contention spikes in production
- Coarse-grained
synchronized— one lock around whole service method; all requests serialize. - Static synchronized singleton — accidental global lock.
- Wrong lock object — locking on
Integercache or shared string literal. - Slow work inside synchronized block — DB call or HTTP while holding lock.
- Hidden library lock — legacy cache, logger, SimpleDateFormat (not thread-safe) wrapped in sync.
- Thread pool + synchronized — many workers pile up on same monitor; looks like pool issue but root is lock.
BLOCKED ≠ deadlock. Deadlock is a cycle of locks; many BLOCKED threads with one clear owner is contention. See deadlock guide for circular waits.
What — identify the lock and owner (in order)
-
Capture 2–3 thread dumps 10s apart
jcmd <pid> Thread.print > /tmp/td1.txt sleep 10 jcmd <pid> Thread.print > /tmp/td2.txt
Threads blocked on same lock in all dumps → chronic contention, not transient. -
Find a BLOCKED thread and read “waiting to lock”
"http-nio-8080-exec-12" #45 BLOCKED at com.app.Service.process(Service.java:88) - waiting to lock <0x00000000f1234ab0> (a com.app.Service) at ...
Address0x00000000f1234ab0is the monitor identity. -
Search dumps for that hex address as “locked”
grep "0x00000000f1234ab0" td1.txt
Owner line example:"http-nio-8080-exec-3" #12 RUNNABLE ... - locked <0x00000000f1234ab0> (a com.app.Service)
If no owner but many waiters → JVM still starting lock or dump race; capture again. -
Read owner’s stack—what is it doing while holding the lock?
If owner is in JDBC, HTTP, or heavy CPU inside
synchronized, that is the bug. - Count waiters on same monitor 50+ BLOCKED on one lock → hotspot; prioritize shrinking or removing that lock.
-
Check for AQS / ReentrantLock WAITING
Stack contains
AbstractQueuedSynchronizer.acquire→java.util.concurrent.locksissue. -
Correlate with metrics
Tomcat thread pool all busy; latency up; lock profiling (JFR event
java.monitor.Wait) if available. -
Rule out “BLOCKED” misread
Threads in
TIMED_WAITINGon pool queue may be pool exhausted—not monitor BLOCKED. See thread pool exhausted.
Worked example (reading the dump)
"pool-1-thread-5" BLOCKED on monitor for SimpleDateFormat → 40 http threads BLOCKED on same monitor Owner "pool-1-thread-2" RUNNABLE - locked SimpleDateFormat - at java.text.SimpleDateFormat.format(...) - at com.app.ReportBuilder.build(...)
Fix: replace SimpleDateFormat with DateTimeFormatter (immutable) or per-thread instance—not synchronize on shared formatter.
Tools
- jstack / jcmd Thread.print — free, always available.
- JFR — Monitor Enter / Java Monitor Wait events with stack traces.
- async-profiler —
-e lockfor lock contention profiling. - IntelliJ / VisualVM — analyze thread dumps visually.
How — fix contention and prevent recurrence
Fix patterns
| Problem | Fix |
|---|---|
I/O inside synchronized | Move lock after DB/HTTP; only guard in-memory state |
| Global lock on service | Finer locks per key shard; ConcurrentHashMap |
| Non-thread-safe helper | Use thread-safe API (DateTimeFormatter) |
| Read-heavy shared map | ReadWriteLock or concurrent copy-on-read structure |
| Lock on shared cache key | Striped locks by hash(key) % N |
| ReentrantLock misuse | Try tryLock(timeout); fail fast with 503 |
Code direction (before / after)
// Bad: whole request serialized
public synchronized Response handle(Request r) {
return db.query(r); // holds lock during I/O
}
// Better: no lock on I/O; only guard shared mutable state
public Response handle(Request r) {
Data d = db.query(r);
return buildResponse(d); // immutable or local
}
// Shared counter: use atomic or lock only increment
private final LongAdder count = new LongAdder();
Verify
- Load test at prior peak RPS.
- Thread dumps: zero or few BLOCKED on former monitor.
- Latency p99 down; throughput up without more pods.
- JFR: monitor wait time near zero on hot path.
Prevention
- Code review: flag
synchronizedon service/controller classes. - Ban shared
SimpleDateFormat, static mutable collections without sync strategy. - Staging test with thread dump sampling under load before release.
- Alert: all Tomcat threads busy + growing queue depth.
Interview one-liner
“BLOCKED means waiting for a monitor. I take thread dumps, get the ‘waiting to lock’ hex address, grep for the thread that ‘locked’ it, and inspect whether the owner does slow work while holding the lock—then I shrink the critical section or use concurrent structures.”