Capture a thread dump of the running JVM and look for the JVM's deadlock report, or query it programmatically with ThreadMXBean.findDeadlockedThreads(). The JVM detects monitor (and ReentrantLock/AQS) deadlock cycles for you — it just reports rather than resolves them.
Thread dumps from outside the JVM
Given the process id, any of these prints a full dump; the JVM scans the lock graph and appends a deadlock section if a cycle exists:
jcmd <pid> Thread.print # modern, preferred
jstack <pid> # classic
jstack -l <pid> # -l also lists java.util.concurrent ownable synchronizers
kill -3 <pid> # SIGQUIT -> dump goes to the process's stdout
jcmd <pid> Thread.print and jstack both emit a Found one Java-level deadlock: block naming the threads and the locks in the cycle. -l is important when ReentrantLock is involved, since those locks only show up in the ownable-synchronizers listing.
Programmatic detection
For a health endpoint or a watchdog, ask the platform MX bean directly:
ThreadMXBean mx = ManagementFactory.getThreadMXBean();
long[] ids = mx.findDeadlockedThreads(); // monitors + ownable synchronizers (e.g. ReentrantLock)
if (ids != null) {
ThreadInfo[] infos = mx.getThreadInfo(ids, true, true);
for (ThreadInfo info : infos) {
System.out.println(info.getThreadName()
+ " blocked on " + info.getLockName()
+ " owned by " + info.getLockOwnerName());
}
}
// Note: findMonitorDeadlockedThreads() finds only intrinsic-lock cycles;
// findDeadlockedThreads() covers ReentrantLock/AQS too — prefer it.
findDeadlockedThreads() returns null when there is no deadlock, so it is cheap to poll periodically and alert.