Java / Editorial solution guide

Diagnose races, deadlocks, and thread leaks

Classify incorrect shared results, blocked dependency cycles, and missing worker cleanup using bounded reproductions and protected runtime evidence.

About 4 min readContent reviewed

Read-only knowledge base ยท No public comments

Recognize the symptoms

A race may produce lost increments, stale state, or inconsistent multi-field snapshots without throwing any exception. A deadlock may leave tasks waiting indefinitely in a dependency cycle.

A thread leak may show growing live-thread counts or a JVM that remains alive after useful work finishes. High thread count alone is not proof of a leak, and Java 21 virtual-thread workloads need version-aware interpretation.

Understand the likely causes

  • Shared mutation lacks a consistent synchronization protocol, tasks acquire resources in opposite orders, or workers block waiting for queued work in the same saturated executor.
  • Executors are created repeatedly or never shut down, interruption is swallowed, or tasks retain resources and cannot complete. Timing sleeps can hide a race without fixing its allowed interleaving.

Classify behavior and preserve the workload

Record runtime release, request concurrency, executor configuration, queue policy, task identity, and the observed correctness or latency symptom. Separate a wrong result from ordinary waiting and from a proven dependency cycle. Reproduce with synthetic data in a process you own, with a test timeout. Do not attach tools to configured application processes as part of this public example or publish raw dumps containing secrets.

Explain the invalid interleaving

For count++, show two readers observing the same old value before both write. volatile alone does not make this compound operation atomic. Use one monitor protocol for the invariant or an appropriate atomic operation. The example performs a bounded atomic update and prints only after join establishes completion. A fixed result should never be fabricated for the broken unsynchronized version.

java An atomic update with explicit completion
public class Main {

 public static void main(String[] args) throws Exception {
var count=new java.util.concurrent.atomic.AtomicInteger();
Thread worker=new Thread(()->{for(int i=0;i<100;i++) count.incrementAndGet();});
worker.start();worker.join();
System.out.println(count.get());
 }
}
Expected output
100

Every increment is atomic and the result is read after the worker terminates. A multi-field invariant would require a wider design.

Trace owners and requested resources

For suspected deadlock, collect authorized thread evidence through the deployed JDK's documented tools and follow each waiting task to its resource owner until a cycle is established. A blocked thread by itself is not proof. Monitor locks are not the only source: a one-worker executor waiting for another task queued to itself can starve without a monitor deadlock. Use consistent resource ordering or remove the blocking dependency as the evidence requires.

Repair worker ownership and cancellation

Assign each executor one owner and an explicit shutdown deadline. shutdown stops new admission; shutdownNow attempts interruption rather than safely killing arbitrary code. Propagate InterruptedException or restore the flag when propagation is impossible. Bound queues and outstanding tasks so overload does not resemble an endless leak. Java 17 requires explicit ExecutorService cleanup; newer AutoCloseable examples must be checked against their labeled API release.

During a leak investigation, state the expected owner and lifetime of each worker group. A small stable pool waiting for future work may be healthy; creating another pool for every request is different. Check whether completed requests release their workers and captured request data before changing limits. This links the observed trend to an accountable lifecycle decision.

Interpret repeated snapshots and virtual-thread evidence

Compare repeated bounded snapshots under equivalent load to distinguish normal idle workers from accumulating unfinished tasks. JFR and thread dumps have release-specific coverage, especially for virtual threads; use the exact deployed documentation. Protect captured stack traces and task arguments. Evidence should identify a retaining owner, a dependency cycle, or a violated shared-state invariant rather than merely a large number in one screen.

Verify the fix

Verify the specific interleaving with latches or controlled collaborators instead of sleeps, then run bounded representative concurrency. Check exact final state, task failure observation, queue limits, interrupt handling, and worker termination after shutdown. A race test passing repeatedly is evidence, not a proof of every Java Memory Model execution. Confirm no duplicate external effects occur after cancellation or retries.

Keep it from recurring

  • Prefer immutable messages or thread confinement where they simplify ownership. Document synchronization for shared state, executor admission and lifecycle, and version-specific diagnostics. Add regression cases for the actual incident boundary rather than relying on increasing pool size or timeout indefinitely.

Official references