Java Concurrency & Async Programming · Lesson 14

Production Java Concurrency Troubleshooting

Diagnose Java thread starvation, unbounded queues, lock contention, virtual-thread pinning, downstream overload, and failed cancellation with evidence.

Concurrency incidents rarely announce themselves as “a thread bug.” Users see timeouts, partial results, duplicates, or an unavailable service. The same symptom can come from CPU saturation, worker starvation, a growing queue, lock contention, a connection bottleneck, downstream overload, pinned virtual threads, or work that ignored cancellation.

Quick answer

Start from user impact and a precise time window. Separate admitted, queued, running, resource-waiting, completed, rejected, timed-out, and post-deadline work. Correlate application metrics and traces with JFR, jcmd, executor state, locks, connection acquisition, CPU, and downstream evidence. Mitigate at the constrained boundary, preserve durable outcomes, and verify recovery. More threads are not a diagnosis.

Learning objectives

  • Distinguish starvation, convoying, lock contention, pinning, CPU saturation, downstream overload, and failed cancellation from bounded production evidence.
  • Apply reversible mitigation and verify JVM, dependency, and user-visible recovery without duplicating durable side effects.

Prerequisites

Review Java Virtual Threads for Backend Services, ExecutorService and Work Queues, and Java Interruption and Deadlines. This lesson is shared with Spring Backend and System Design, while its primary home is the Java Concurrency course and topic cluster.

Triage from outcomes to constraints

Declare incident scope: affected operation, region, version, start time, error and latency shape, and durable business risk. Then ask:

  1. Is work rejected, queued, running, blocked, or completed after the caller deadline?
  2. Is CPU saturated, or are threads waiting on locks, connections, sockets, queues, or futures?
  3. Did demand increase, service time increase, or available capacity fall?
  4. Which operations may have committed locally or remotely despite a timeout?

Thread counts without state and ownership are weak evidence. A stable 50-thread pool can be completely starved. Ten thousand parked virtual threads can be healthy, while a small number of pinned carriers can matter if pinning is long-lived and frequent.

Java 21 evidence commands

jcmd <pid> Thread.print -l
jcmd <pid> JFR.start name=concurrency settings=profile duration=120s filename=concurrency.jfr
jcmd <pid> VM.version

Run commands through approved operational access, protect captured data, and account for overhead. A thread dump is a snapshot, so compare several samples. JFR reveals duration and correlation that a single dump cannot. For Java 21 virtual-thread pinning investigation, use JFR events and, in a controlled diagnostic environment, -Djdk.tracePinnedThreads=full.

Symptom map

  • Thread-pool starvation: all workers wait for work that itself needs the same pool, or block on a constrained dependency. Queue age rises while completion falls.
  • Unbounded queue: active count looks capped, but admitted backlog, memory, and stale post-timeout work grow.
  • Lock convoy: many threads block behind a long or slow critical section; increasing workers increases waiters.
  • CPU saturation: runnable work and CPU remain high. Virtual threads do not add CPU lanes.
  • Downstream overload: local execution appears available, but connection acquisition and remote latency rise. Admission and a resource guard belong before the dependency.
  • Ignored cancellation: caller timeouts rise while work, connections, or side effects continue after deadline.
  • Virtual-thread pinning: relevant virtual threads block carriers inside pinned regions. Diagnose duration and impact before rewriting synchronization.

Spring boundaries during incidents

Identify every executor: servlet or server execution, @Async, scheduling, messaging, client libraries, and application-owned pools. Verify which one a stack trace represents. Transactions do not automatically cross @Async boundaries. SecurityContext, MDC, and ThreadLocal propagation may be absent or stale; never weaken authorization to restore a trace. Spring Boot’s virtual-thread setting changes selected execution defaults, not database or downstream capacity.

Failure scenario: the “fixed” pool increase

A checkout service times out while 40 request workers wait for a 20-connection pool. An operator doubles both pools. Database CPU saturates, lock waits rise, and throughput falls. Some clients retry timed-out orders that later commit, creating duplicate reservations.

A safer mitigation sheds excess admission, protects status and recovery endpoints, lowers unnecessary retries, and preserves idempotency keys. Operators inspect durable order state before replay. After stabilization, they tune query time, transaction scope, connection limits, and request admission from measured database capacity.

Common misconceptions

  • More workers do not fix a saturated dependency.
  • A low CPU graph does not prove spare request capacity; work may be blocked.
  • A timeout graph does not prove tasks or side effects stopped.
  • Virtual threads do not guarantee lower latency or remove pinning and context concerns.
  • One thread dump or one sampled trace is not complete incident proof.
  • Clearing queues can discard work; do it only with explicit durable recovery ownership.

Production validation

After mitigation, verify user success and latency, rejection, queue age, active work, cancellation observed, work completed after deadline, connection waits, lock duration, CPU, downstream health, and durable business state. Compare JFR evidence before and after. Exercise a canary and rollback threshold. Reconcile ambiguous orders, payments, and messages. Record the constrained resource, why the change was safe, and which evidence would trigger reversal.

Use the Java Concurrency Budget Lab to explain the incident model, not to calculate production limits. Continue with the guided lab examples.

FAQ

Should I take a thread dump first?

First bound user impact and time. Then capture several safe snapshots and JFR evidence so thread state can be correlated with the incident window.

Does a large virtual-thread count prove a leak?

No. Inspect task lifetime, owners, deadlines, resource waits, and completion. Large counts can be expected; unbounded stale work is still dangerous.

When is recovery complete?

When user outcomes, JVM and dependency capacity, and durable business state are verified—not merely when an alert turns green.

Follow the Java Concurrency course, browse the topic cluster, and continue with Production JVM Performance Diagnostics before selecting GC, heap, native-memory, thread, or JIT evidence. Connect incident practice to Production Overload Troubleshooting.

Official sources

Knowledge check

Check your understanding

Answer both questions correctly to mark this lesson as mastered. You can retry without penalty.

1. Request workers wait for a smaller database pool while connection acquisition rises; which mitigation protects useful work?

2. When can operators declare a Java concurrency incident recovered?