Back to Thread Troubleshooting
Q1Question 1 of 5

How to troubleshoot thread contention?

Key Concepts
  • Step 1: collect a thread dump with jstack or programmatically with ThreadMXBean to see all thread states and lock information.
  • Step 2: count threads by state (BLOCKED, WAITING, RUNNABLE) to identify the type of contention.
  • Step 3: find the lock that many threads are blocked on, and identify the lock owner from the thread dump.
  • Step 4: examine the lock owner's stack trace to find the root cause: slow DB query inside synchronized, undersized pool, or slow downstream.
  • Four root causes: excessive locking (coarse lock or long hold time), undersized thread pool, slow downstream (HTTP/DB), and DB connection pool contention.
  • JDK Flight Recorder (JFR) provides continuous recording of lock contention events, hot methods, and thread park durations.
  • The most common production fix is moving IO operations outside synchronized blocks to reduce lock hold time.

Definition

Troubleshoot thread contention by collecting thread dumps (jstack), counting BLOCKED threads, finding the lock owner, and diagnosing the root cause: excessive locking, undersized pool, slow downstream, or connection pool exhaustion.
  • Thread dump – a snapshot of all threads' states, stack traces, and lock information. Collected with jstack , kill -3 , or ThreadMXBean.dumpAllThreads().
  • Thread states – many BLOCKED = lock contention. Many WAITING = pool workers idle or waiting for downstream. Many TIMED_WAITING = threads sleeping or on timeout.
  • Lock identification – ThreadInfo.getLockName() shows which lock threads are blocked on. getLockOwnerName() shows the thread holding the lock.
  • Root causes – (1) excessive locking: coarse lock or slow operation inside synchronized, (2) undersized pool: all workers busy, queue growing, (3) slow downstream: threads blocked on HTTP/DB calls, (4) connection pool contention: all connections in use, threads waiting for free connection.
  • JFR – JDK Flight Recorder provides continuous recording with low overhead. Lock contention events, thread park durations, and hot methods are all captured.

The bank is slow and customers are complaining. The manager investigates: (1) take a photo of the entire bank (thread dump), (2) count how many tellers are standing at the vault door waiting (BLOCKED), (3) find who is inside the vault holding everyone up (lock owner), (4) figure out why that person is slow: counting coins manually (CPU-bound)? waiting for a fax response (slow downstream)? the vault door is too narrow (coarse lock)?

Understanding the Concepts

How do you troubleshoot thread contention?

The systematic approach has four steps: collect evidence, analyze thread states, identify the bottleneck, and fix the root cause.

Step 1: Collect a thread dump

Take multiple dumps (3-5) spaced 5 seconds apart. A single dump is a snapshot; multiple dumps show trends and distinguish genuinely stuck threads from normal busy work.

BASH
jstack -l <PID> > dump1.txt
sleep 5
jstack -l <PID> > dump2.txt
sleep 5
jstack -l <PID> > dump3.txt

Or programmatically from inside the application:

JAVA
package com.bank.diagnostics;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class ThreadDumpCollector {
    public static void dumpThreads() {
        ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
        ThreadInfo[] infos = tmx.dumpAllThreads(true, true);
        for (ThreadInfo info : infos) {
            System.out.println(info.getThreadName() + ": " + info.getThreadState());
            if (info.getLockName() != null)
                System.out.println("  Blocked on: " + info.getLockName());
            if (info.getLockOwnerName() != null)
                System.out.println("  Lock held by: " + info.getLockOwnerName());
        }
    }
}

Step 2: Count thread states

Many BLOCKED threads on the same lock indicates lock contention. All pool workers RUNNABLE with a growing queue indicates an undersized pool. Many TIMED_WAITING in SocketInputStream.read indicates slow downstream services.

BASH
# Quick count of thread states
grep -c 'BLOCKED' dump.txt
grep -c 'WAITING' dump.txt
grep -c 'RUNNABLE' dump.txt
grep -c 'TIMED_WAITING' dump.txt

Step 3: Identify the lock owner

The thread dump shows which lock each BLOCKED thread is waiting for and which thread currently holds that lock. The lock owner's stack trace reveals what the holder is doing while other threads wait.

Step 4: Fix the root cause

Four common root causes with different fixes:

| Root cause | Thread dump signature | Fix |
|---|---|---|
| Excessive locking | 150 BLOCKED on one lock, owner in DB query | Move IO outside synchronized block |
| Undersized pool | All workers RUNNABLE, queue 5000+ deep | Increase pool size or optimize task duration |
| Slow downstream | 100 TIMED_WAITING in SocketInputStream | Add timeouts (5s), add circuit breaker |
| Connection pool | 100 WAITING in HikariPool.getConnection | Increase maxPoolSize, optimize queries |

How do you fix excessive locking in a banking transfer service?

The most common production fix is moving IO operations outside the synchronized block to minimize lock hold time.

JAVA
package com.bank.service;

import java.math.BigDecimal;

public class TransferService {

    // WRONG: DB query inside synchronized block
    // Lock hold time: 2 seconds (DB query time)
    public void transferWrong(String fromId, String toId, BigDecimal amount) {
        synchronized (this) {
            BigDecimal fromBalance = accountRepo.getBalance(fromId);  // 2s DB query!
            BigDecimal toBalance = accountRepo.getBalance(toId);      // 2s DB query!
            accountRepo.updateBalance(fromId, fromBalance.subtract(amount));
            accountRepo.updateBalance(toId, toBalance.add(amount));
        }
    }

    // CORRECT: only synchronize the critical section
    // Lock hold time: microseconds (in-memory check + update)
    public void transferCorrect(String fromId, String toId, BigDecimal amount) {
        BigDecimal fromBalance = accountRepo.getBalance(fromId);  // outside lock
        BigDecimal toBalance = accountRepo.getBalance(toId);      // outside lock
        synchronized (this) {
            // Only synchronize the state mutation
            accountRepo.updateBalance(fromId, fromBalance.subtract(amount));
            accountRepo.updateBalance(toId, toBalance.add(amount));
        }
    }
}

What is JFR and how does it help?

JDK Flight Recorder provides continuous recording with low overhead (typically under 2% CPU). It captures lock contention events, thread park durations, hot methods, and GC pauses. Analyze recordings with JDK Mission Control.

BASH
# Start a JFR recording for 60 seconds
jcmd <PID> JFR.start duration=60s filename=contention.jfr
# Open in JDK Mission Control: "Lock Instances" tab shows most contended locks

What are common interview follow-up questions?

  • How often should you take thread dumps? Take 3-5 dumps 5 seconds apart. A single dump is a snapshot; multiple dumps show trends and distinguish stuck threads from momentarily busy ones.
  • What is the difference between BLOCKED and WAITING? BLOCKED means waiting for a synchronized monitor. WAITING means parked by LockSupport, Object.wait(), or Thread.join(). ReentrantLock contention shows as WAITING, not BLOCKED.
  • How do you detect ReentrantLock contention? Use JFR jdk.ThreadPark events or async-profiler -e lock mode, since ReentrantLock waiters show as WAITING (parking), not BLOCKED.

Visual Overview

DIAGRAM
  Thread Contention Troubleshooting Flow
  ══════════════════════════════════════

  Step 1: jstack <PID>  →  thread_dump.txt
  Step 2: Count states:
    BLOCKED: 150    ← lock contention!
    WAITING: 50     ← pool idle / downstream
    RUNNABLE: 30    ← actually working
  Step 3: BLOCKED on which lock?
    → com.bank.AccountService@0x7f...
    → Held by: http-nio-exec-5
    → Stack: AccountService.transfer() → DB query inside sync!
  Step 4: Fix → move DB query outside synchronized

Key Takeaway

Troubleshooting thread contention follows a systematic four-step process. Collect thread dumps with jstack. Count thread states to identify the type of problem. Find the lock that many threads are blocked on and identify the lock owner. Examine the owner's stack trace to find the root cause.

Four root causes have different fixes: excessive locking (move IO outside synchronized), undersized pool (increase workers or optimize task duration), slow downstream (add timeouts and circuit breakers), and connection pool contention (increase HikariCP maxPoolSize and optimize queries).

At National Bank of India, P99 latency spiked 100x because a 2-second DB query was running inside a synchronized block, blocking 180 other threads. Moving the query outside the synchronized block and only synchronizing the balance update restored P99 in 5 minutes.

Remember one sentence: take a thread dump, count BLOCKED threads, find the lock owner, and fix the root cause by reducing lock hold time or sizing resources correctly.

---

Share
Use ← → arrow keys to navigate