【问题标题】:Understanding deadlock with a simple example用一个简单的例子理解死锁
【发布时间】:2019-02-18 19:03:33
【问题描述】:
我正在努力理解死锁的基础知识,所以我想出了下面的代码。我有两个线程以相反的顺序获取锁,但它们没有死锁。当我运行它时,我会看到所有的打印输出。我做错了什么?
public class DeadlockBasics {
private Lock lockA = new ReentrantLock();
private Lock lockB = new ReentrantLock();
public static void main(String[] args) {
DeadlockBasics dk = new DeadlockBasics();
dk.execute();
}
private void execute() {
new Thread(this::processThis).start();
new Thread(this::processThat).start();
}
// called by thread 1
public void processThis() {
lockA.lock();
// process resource A
System.out.println("resource A -Thread1");
lockB.lock();
// process resource B
System.out.println("resource B -Thread1");
lockA.unlock();
lockB.unlock();
}
// called by thread 2
public void processThat() {
lockB.lock();
// process resource B
System.out.println("resource B -Thread2");
lockA.lock();
// process resource A
System.out.println("resource A -Thread2");
lockA.unlock();
lockB.unlock();
}
}
【问题讨论】:
标签:
java
multithreading
deadlock
reentrantlock
【解决方案1】:
首先没有保证哪个线程首先启动。要获得死锁,其中一个线程必须锁定lockA,然后第二个线程必须锁定lockB,反之亦然。
public void processThis() {
lockA.lock();
// here the control should be switched to another thread
System.out.println("resource A -Thread1");
lockB.lock();
...
但是可能没有足够的时间在线程之间切换,因为您只有几行代码。它太快了。为了模拟一些长时间的工作,在两个方法的第二个锁定之前添加延迟
lockA.lock();
Thread.sleep(200); // 200 milis
那么第二个线程就能在第一次发布之前锁定lockB
【解决方案2】:
这确实会导致死锁,但并非总是如此,例如,如果 processThis() 完全执行,然后 processThat() 或反之亦然,则没有死锁。您可以尝试添加 Thread.delay(100) 或 Thread.yield() 来引导线程执行走向死锁,甚至解除对某个死锁的解锁。
【解决方案3】:
您的代码是死锁的一个很好的例子,因为 ReenttrantLock 是一个互斥锁,其行为与使用同步的隐式监视器锁访问相同。但是,由于这一部分,您看不到死锁:
private void execute() {
new Thread(this::processThis).start();
new Thread(this::processThat).start();
}
创建并启动第一个线程后,创建第二个线程需要一段时间。 JVM 大约需要 50 us 甚至更少的时间来创建一个新线程,这听起来很短,但是对于完成第一个线程来说已经足够了,因此不会发生死锁。
我在您的代码中添加了Thread.sleep();,这样两个线程就可以以某种方式并行执行。
package com.company;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockBasics {
private Lock lockA = new ReentrantLock();
private Lock lockB = new ReentrantLock();
public static void main(String[] args) {
DeadlockBasics dk = new DeadlockBasics();
dk.execute();
}
private void execute() {
new Thread(this::processThis).start();
new Thread(this::processThat).start();
}
// called by thread 1
private void processThis() {
lockA.lock();
// process resource A
try {
Thread.sleep(1000); //Wait for thread 2 to be executed
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 will own lock a");
lockB.lock();
// process resource B
System.out.println("Thread 1 will own lock b");
lockA.unlock();
lockB.unlock();
// Both locks will now released from thread 1
}
// called by thread 2
private void processThat() {
lockB.lock();
// process resource B
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2 will own lock b");
lockA.lock();
// process resource A
System.out.println("Thread 2 will own lock a");
lockA.unlock();
lockB.unlock();
// Both locks are released by thread 2
}
}
【解决方案4】:
两点:
- 以获取锁的相反顺序释放锁。也就是说,
processThis 应该颠倒删除锁的顺序。对于您的示例,顺序无关紧要。但是,如果processThis 在释放 B 上的锁之前尝试在 A 上获取新锁,则可能会再次发生死锁。更一般地,您会发现通过考虑锁的范围并避免重叠但非封闭的范围来更容易考虑锁。
- 为了更好地突出问题,我会在每个线程中获取第一个锁后调用
wait,并让execute 启动两个线程然后在两个线程上调用notify线程。