Analyzing & Troubleshooting Blocked Threads Using JFR 

Java Flight Recorder (JFR) provides continuous, low-overhead profiling by capturing runtime execution samples directly within the JVM. In this blog, we will examine Blocked Threads, a performance issue commonly caused by one thread holding a lock indefinitely while other threads wait to acquire that same lock, unable to proceed. We will simulate the problem, capture the relevant JFR data, and analyze the recording to identify the underlying issue. Let’s take a closer look.

What are Blocked Threads?

Fig: Threads wait in the BLOCKED state until the lock is released 

A thread enters the BLOCKED state when it needs a resource that another thread already holds. It cannot move forward until that resource is released. This happens when threads contend for a synchronized method, a synchronized block, or a ReentrantLock. In each case, the waiting thread stays BLOCKED until the thread holding the resource lets go of it. 

What causes ‘Blocked Threads’?

Let’s look at the list of causes for this Blocked Threads issue: 

  1. Lock Contention on a Synchronized Method or Block: Threads enter the BLOCKED state whenever they need a lock that another thread is already holding, whether that lock guards a synchronized method, a synchronized block, or a ReentrantLock. In a busy application, this kind of contention happens constantly and is usually resolved in a moment.
  2. A Lock Held Too Long by a Slow or Stuck Thread: The real trouble starts when the thread holding the lock is delayed, often because it is waiting on I/O, a network call, or some other slow resource while still inside the locked section. Every other thread needing that same lock queues up behind it.
  3. A Large Number of Threads Competing for One Lock: In a high-concurrency system, hundreds of threads can end up blocked on a single lock at once, which turns what would normally be a brief wait into a serious performance problem. 

Simulating Blocked Threads Performance Issue

To understand how Blocked Threads appears in JFR data, let’s reproduce the issue using a sample Java application. The following program deliberately launches ten threads that all call into the same static synchronized method. The first thread to get there acquires the lock and loops forever, so the other nine threads are left waiting indefinitely to enter.

BlockedAppDemo launches ten competing threads.

public class BlockedAppDemo {
public static void start() {
System.out.println("App started");
for (int counter = 0; counter < 10; ++counter) {
// Launch 10 threads.
new AppThread().start();
}
System.out.println("App became unresponsive");
}
public static void stop() {
System.out.println("Blocked App problem terminated!");
}
}

Each AppThread calls into the same shared object.

public class AppThread extends Thread {
@Override
public void run() {
AppObject.getSomething();
}
}

AppObject locks itself and never releases it.

public class AppObject {
private static boolean flag = true;
public static void setFlag(boolean newValue) {
flag = newValue;
}
public static synchronized void getSomething() {
// Put the thread to sleep forever. Basically first
// thread would have acquired the lock and go to sleep
// No other thread would be able to enter this method.
while (flag) {
try {
Thread.sleep(10 * 60 * 1000);
} catch (Exception e) {}
}
}
}

In this program, start() launches ten instances of AppThread, and every one of them calls the static synchronized method getSomething() on AppObject. Because the method is static synchronized, calling it locks the entire AppObject class for the duration of the call. Whichever thread arrives first acquires that class level monitor and enters the while (flag) loop. Since flag is never set to false, this thread sleeps in ten-minute intervals indefinitely, holding the lock the entire time. The remaining nine threads reach getSomething() immediately after, find the monitor already taken, and drop into the BLOCKED state, waiting on a method that will never free up. The application prints “App became unresponsive,” and from that point on, it is.

Capturing JFR Data for Troubleshooting Blocked Threads

To capture JFR data for troubleshooting the Blocked Threads issue, we recommend that you follow the steps below:

If you are interested in learning about the other methods available to capture JFR recordings, we recommend that you read ‘How to Capture Java Flight Recorder (JFR)?’ blog.

Step 1: Start a JFR recording against the running JVM:

jcmd {PID} JFR.start \
name=loadTestCapture \
settings=profile \
filename=/tmp/tomcat.jfr

Note: Replace <PID> with the Process ID (PID) of your Java application running inside the container. If you don’t know how to find it, refer to our guide on finding the Java application Process ID (PID) for step-by-step instructions.

Step 2: Let it run while the Blocked Threads is occurring, then stop it manually:

jcmd {PID} JFR.stop \
name=loadTestCapture

Step 3: Alternatively, for Fixed-Duration Capture,  you can start a recording that automatically exits after a predefined duration (for example, 15 minutes) in a single step:

jcmd {PID} JFR.start \
name=loadTestCapture \
settings=profile \
duration=15m \
filename=/tmp/tomcat.jfr

Analyzing Blocked Threads Using the JFR Data

You can analyze JFR recording using yCrash JFRPlayer by following the steps mentioned below.  

Step 1: Install yCrash JFRPlayer, which is available in both cloud and on-premises versions. Use one of the options below to get started:

  • Cloud service: Register and upload your JFR file online.
  • On-Premises: Install and run yCrash JFRPlayer on your local machine or within your organization’s environment.

Step 2: Upload the jfr file to your yCrash JFRPlayer. Once JFR file is uploaded, JFRPlayer parses the JFR file and generates an incident report instantly. 

Fig: Uploading a standalone JFR file to yCrash JFRPlayer 

Step 3: I’d recommend you review the AI overview section first, as it gives you an executive summary of the issue in plain language, along with the Root Cause Analysis (RCA). In this case, it flags a single thread, Thread-1, holding a lock on com.buggyapp.blockedapp.AppObject while stuck in a sleep state, leaving 9 other threads BLOCKED and unable to proceed. 

Fig: yCrash JFRPlayer AI Overview identifying Thread-1 as the culprit blocking 9 other threads 

Step 3: Next is to check the “Issues in the Application” section, which flags every problem individually. This entry is flagged WARNING, telling us directly that Thread-1 obtained the lock on AppObject and never released it, causing 9 other threads to become blocked. It also links straight to the stack trace showing why.

Fig: yCrash JFRPlayer flagging the blocked thread issue, with a direct link to Thread-1’s stack trace 

Step 4: Click through to the Thread section in the report to see the Blocking Threads Transitive Graph, which shows Thread-1 at the center, directly blocking Thread-2 through Thread-10, all 9 of the remaining threads in one flat cluster. This is the key visual difference from Deadlock: one culprit thread radiating out to every waiter, not two threads each blocked on the other. Clicking into Thread-1 itself confirms the detail: it sits in TIMED_WAITING, called through AppThread.run() into AppObject.getSomething(), and is locked inside Thread.sleep(), holding the AppObject class lock the entire time.

Fig: yCrash JFRPlayer Blocking Threads Transitive Graph showing Thread-1 blocking all 9 other threads 

Fig: Thread-1 sleeping (TIMED_WAITING) while retaining the AppObject lock.

Simple, right? Now that we have analyzed the data, we are equipped with all the information to fix the problem.

If you face any challenges while analyzing a JFR file using yCrash JFRPlayer, check out our FAQ for answers to common questions and troubleshooting guidance.

How to fix Blocked Threads

The following are potential solutions to fix this issue:

  1. Avoid Holding a Lock During a Long or Indefinite Wait: A thread should never sleep, block on I/O, or wait indefinitely while still holding a lock other threads need. Release the lock first, then wait.
  2. Give the Lock Holder a Real Exit Condition: The while (flag) loop here has no path to false during normal execution. Any loop that runs inside a synchronized method needs a guaranteed way to terminate.
  3. Use Timed Lock Attempts Instead of Indefinite Blocking: Replacing synchronized with Lock.tryLock() and a timeout lets waiting threads give up and recover instead of sitting BLOCKED forever.
  4. Reduce Lock Scope: Narrow the synchronized section to only the code that truly needs protection, so a slow operation like Thread.sleep() or a network call isn’t performed while a shared lock is held.
  5. Monitor for Long-Held Locks: A JFR recording or thread dump showing one thread consistently holding a lock while many others queue up behind it, as seen in this example’s blocking threads graph, is a clear signal to investigate that lock’s scope.

Conclusion

Diagnosing and identifying the root cause of Blocked Threads can be challenging, especially in complex production environments where multiple symptoms often overlap. In this example, the JFR recording was analyzed using yCrash JFRPlayer, which automatically identified the key performance bottlenecks and correlated JVM events to pinpoint the underlying problem. The analysis traced the issue to Thread-1 holding the lock on AppObject indefinitely, leaving nine other threads BLOCKED and unable to proceed. By examining the relevant JFR insights, including execution samples, thread activity, monitor contentions, lock ownership, we were able to identify the root cause and determine the appropriate fix.

Share your Thoughts!

Up ↑

Discover more from yCrash

Subscribe now to keep reading and get access to the full archive.

Continue reading