Issues with Locking - DeadLock¶
Locking mechanisms in Java, while essential for ensuring thread safety in multithreaded applications, can introduce various issues if not used properly.
In this section, we’ll explore how deadlocks occur, how to prevent them, and practical examples of various techniques to detect and resolve deadlocks. A deadlock is a common concurrency issue in multithreaded programs and can severely impact performance.
What is Deadlock?¶
A deadlock occurs when: 1. Two or more threads are blocked indefinitely. 2. Each thread is waiting for a lock held by the other, and neither can proceed.
This results in a circular wait, where no thread can release the locks it holds, leading to a deadlock condition.
How Deadlock Occurs: Step-by-Step Explanation¶
Let’s revisit the classic deadlock example.
Example of Deadlock in Java¶
class A {
public synchronized void methodA(B b) {
System.out.println(Thread.currentThread().getName() + ": Locked A, waiting for B...");
try {
Thread.sleep(50); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
b.last(); // Waiting for lock on object B
}
public synchronized void last() {
System.out.println(Thread.currentThread().getName() + ": Inside A.last()");
}
}
class B {
public synchronized void methodB(A a) {
System.out.println(Thread.currentThread().getName() + ": Locked B, waiting for A...");
try {
Thread.sleep(50); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
a.last(); // Waiting for lock on object A
}
public synchronized void last() {
System.out.println(Thread.currentThread().getName() + ": Inside B.last()");
}
}
public class DeadlockDemo {
public static void main(String[] args) {
A a = new A();
B b = new B();
Thread t1 = new Thread(() -> a.methodA(b), "Thread 1");
Thread t2 = new Thread(() -> b.methodB(a), "Thread 2");
t1.start();
t2.start();
}
}
How This Deadlock Occurs (Flow Analysis)¶
-
Thread 1 starts and calls
a.methodA(b)
. It acquires the lock on objectA
and prints:
-
Thread 2 starts and calls
b.methodB(a)
. It acquires the lock on objectB
and prints:
-
Now:
- Thread 1 holds the lock on
A
and waits for Thread 2 to release the lock onB
. - Thread 2 holds the lock on
B
and waits for Thread 1 to release the lock onA
.
Both threads are waiting indefinitely, resulting in a deadlock.
How to Avoid Deadlocks¶
- Acquiring Locks in a Consistent Order
- Using
tryLock()
with Timeout - Avoid Nested Locks
- Using Lock Ordering Techniques
1. Solution: Acquiring Locks in a Consistent Order¶
If all threads acquire locks in the same order, deadlock can be prevented.
Modified Example: Acquiring Locks in the Same Order
class A {
public void methodA(B b) {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + ": Locked A, waiting for B...");
synchronized (b) {
System.out.println(Thread.currentThread().getName() + ": Acquired lock on B");
b.last();
}
}
}
public void last() {
System.out.println(Thread.currentThread().getName() + ": Inside A.last()");
}
}
class B {
public void methodB(A a) {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + ": Locked B, waiting for A...");
synchronized (a) {
System.out.println(Thread.currentThread().getName() + ": Acquired lock on A");
a.last();
}
}
}
public void last() {
System.out.println(Thread.currentThread().getName() + ": Inside B.last()");
}
}
public class DeadlockResolved {
public static void main(String[] args) {
A a = new A();
B b = new B();
Thread t1 = new Thread(() -> a.methodA(b), "Thread 1");
Thread t2 = new Thread(() -> b.methodB(a), "Thread 2");
t1.start();
t2.start();
}
}
- Explanation: Both threads now acquire locks in the same order (
A
→B
). This ensures that deadlock cannot occur.
2. Solution: Using tryLock()
with Timeout¶
The tryLock()
method attempts to acquire a lock and fails gracefully if the lock is not available within a specified time.
Example of Deadlock Prevention using tryLock()
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockDemo {
private final ReentrantLock lockA = new ReentrantLock();
private final ReentrantLock lockB = new ReentrantLock();
public void methodA() {
try {
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + ": Locked A");
Thread.sleep(50); // Simulate some work
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + ": Locked B");
} finally {
lockB.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + ": Could not acquire lock B, releasing A");
}
lockA.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void methodB() {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + ": Locked B");
Thread.sleep(50); // Simulate some work
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + ": Locked A");
} finally {
lockA.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + ": Could not acquire lock A, releasing B");
}
lockB.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
TryLockDemo demo = new TryLockDemo();
Thread t1 = new Thread(demo::methodA, "Thread 1");
Thread t2 = new Thread(demo::methodB, "Thread 2");
t1.start();
t2.start();
}
}
- Explanation: If a thread fails to acquire a lock within the timeout, it releases any locks it holds, avoiding a deadlock.
3. Detecting Deadlocks Using Monitoring Tools¶
You can detect deadlocks using tools like: 1. VisualVM: A monitoring tool bundled with the JDK. 2. JConsole: Also part of the JDK, useful for tracking deadlocks in running applications.
4. Best Practices to Avoid Deadlocks¶
- Use
tryLock()
with timeout to avoid indefinite blocking. - Minimize nested locks to reduce the chances of deadlock.
- Acquire locks in a consistent order across all threads.
- Use lock-free data structures like
AtomicInteger
orConcurrentHashMap
when possible. - Analyze your code for potential deadlock scenarios.
Summary¶
Deadlocks are one of the most common and dangerous issues in multithreaded programming. By:
1. Acquiring locks in a consistent order,
2. Using tryLock()
with timeouts,
3. Avoiding nested locks, and
4. Monitoring with tools like VisualVM and JConsole,