Definition
- Detection via jstack –
jstackdumps all thread states and stack traces. The JVM automatically detects deadlocks and prints "Found one Java-level deadlock" with the involved threads and locks. - Programmatic detection –
ThreadMXBean.findDeadlockedThreads()returns an array of deadlocked thread IDs, or null if none. Schedule this in a health-check thread. - tryLock prevention –
ReentrantLock.tryLock(timeout, unit)returns false if the lock cannot be acquired within the timeout, allowing the thread to back off instead of blocking forever. - ReadWriteLock –
ReentrantReadWriteLockallows multiple readers concurrently (read lock is shared) while writers take exclusive access. This reduces the contention that leads to deadlock. - Lock ordering – the primary prevention strategy: always acquire locks in a deterministic order to break circular wait.
Detection is like the bank manager doing a roll call: "Who is standing where, holding what key, waiting for what?" The manager finds the circular dependency. Prevention is a combination: the bank rule says always enter the lower-numbered vault first (lock ordering). If you cannot get a key within 5 minutes, step back and try later (tryLock). For reading the register, multiple tellers can read simultaneously (ReadWriteLock).
Understanding the Concepts
How do you detect a deadlock with jstack?
jstack is a command-line tool that comes with the JDK. It dumps the state and stack trace of every thread in the target JVM.
# Find the process ID
jps
# Take a thread dump
jstack 12345
When a deadlock exists, jstack prints a special section at the end.
Found one Java-level deadlock:
=============================
"TransferThread-1":
waiting to lock monitor 0x00007f... (a com.bank.model.Account)
which is held by "TransferThread-2"
"TransferThread-2":
waiting to lock monitor 0x00007f... (a com.bank.model.Account)
which is held by "TransferThread-1"
Java stack information for the threads listed above:
"TransferThread-1":
at com.bank.service.TransferService.transfer(TransferService.java:25)
- waiting to lock <0x000000076ab...> (a com.bank.model.Account)
- locked <0x000000076ac...> (a com.bank.model.Account)
How do you detect deadlocks programmatically?
Schedule a health check that calls ThreadMXBean.findDeadlockedThreads() every 30 seconds.
package com.bank.monitoring;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class DeadlockMonitor {
public static void startMonitoring() {
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
long[] ids = tmx.findDeadlockedThreads();
if (ids != null) {
ThreadInfo[] infos = tmx.getThreadInfo(ids, true, true);
StringBuilder sb = new StringBuilder("DEADLOCK DETECTED:\n");
for (ThreadInfo info : infos) {
sb.append(" ").append(info.getThreadName())
.append(" waiting for ").append(info.getLockName())
.append(" held by ").append(info.getLockOwnerName())
.append("\n");
}
System.err.println(sb);
// In production: send to PagerDuty or alerting system
}
}, 0, 30, TimeUnit.SECONDS);
}
}
How does tryLock prevent deadlock?
tryLock(timeout) returns false if the lock is not available within the timeout, allowing the thread to release any locks it holds and retry.
package com.bank.service;
import java.math.BigDecimal;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class SafeTransferService {
public boolean transfer(ReentrantLock fromLock, ReentrantLock toLock,
BigDecimal amount) throws InterruptedException {
for (int attempt = 0; attempt < 3; attempt++) {
boolean gotFrom = false;
boolean gotTo = false;
try {
gotFrom = fromLock.tryLock(100, TimeUnit.MILLISECONDS);
gotTo = toLock.tryLock(100, TimeUnit.MILLISECONDS);
if (gotFrom && gotTo) {
// perform transfer
return true;
}
} finally {
if (gotTo) toLock.unlock();
if (gotFrom) fromLock.unlock();
}
// Random backoff to reduce livelock
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 50));
}
return false; // failed after 3 attempts
}
}
How does ReadWriteLock reduce contention?
In banking, most account operations are reads (balance inquiry, statement view). ReentrantReadWriteLock allows multiple readers concurrently.
package com.bank.service;
import java.math.BigDecimal;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class AccountWithRWLock {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private BigDecimal balance;
public AccountWithRWLock(BigDecimal initialBalance) {
this.balance = initialBalance;
}
public BigDecimal getBalance() {
rwLock.readLock().lock();
try { return balance; }
finally { rwLock.readLock().unlock(); }
}
public void debit(BigDecimal amount) {
rwLock.writeLock().lock();
try { balance = balance.subtract(amount); }
finally { rwLock.writeLock().unlock(); }
}
}
// 100 threads reading balance: ALL concurrent (shared read lock)
// 1 thread debiting: exclusive (write lock blocks readers and other writers)
What is a livelock?
A livelock is similar to a deadlock, but the threads are actively running instead of blocked. They keep retrying without making progress.
Thread-1: tryLock(A) success, tryLock(B) fail, release A, retry
Thread-2: tryLock(B) success, tryLock(A) fail, release B, retry
Both keep retrying forever: running but making no progress.
Fix: add random backoff (Thread.sleep(random)) between retries.
What should a senior engineer know?
- Defense in depth: use lock ordering as primary prevention, tryLock as a safety net, and deadlock monitoring as a last-line detection.
- Thread dump analysis tools: tools like fastThread.io and TDA (Thread Dump Analyzer) parse jstack output and visualize deadlock chains.
- Database deadlocks: databases detect deadlocks and kill one transaction as the victim. Java-level deadlocks have no such automatic resolution.
Visual Overview
Deadlock Detection and Prevention Stack: ┌───────────────────────────────────────────┐ │ LAYER 1: Prevention (lock ordering) │ Primary │ Always acquire locks in sorted order │ ├───────────────────────────────────────────┤ │ LAYER 2: Safety net (tryLock + timeout) │ Secondary │ Give up after deadline, retry with backoff│ ├───────────────────────────────────────────┤ │ LAYER 3: Contention reduction (RWLock) │ Optimization │ Multiple readers, exclusive writers │ ├───────────────────────────────────────────┤ │ LAYER 4: Detection (jstack, ThreadMXBean) │ Alerting │ Scheduled health check every 30 seconds │ └───────────────────────────────────────────┘
Key Takeaway
Deadlock detection uses jstack which automatically prints "Found one Java-level deadlock" with the threads and locks involved. Programmatically, ThreadMXBean.findDeadlockedThreads() returns deadlocked thread IDs for automated monitoring. Prevention uses lock ordering (primary: break circular wait), tryLock with timeout (secondary: break hold-and-wait), ReadWriteLock (reduce contention by allowing concurrent reads), and lock-free structures (eliminate locks entirely).
In production, apply defense in depth: lock ordering prevents most deadlocks, tryLock catches edge cases, and deadlock monitoring alerts when prevention fails. ReadWriteLock is especially valuable in banking where 95% of account operations are reads.
Remember one sentence: detect deadlocks with jstack and ThreadMXBean, and prevent them with consistent lock ordering as the primary strategy and tryLock as the safety net.
---