面试提问:什么是死锁或者说什么情况会发生死锁?
死锁一般发生在多线程执行的过程中,不同线程争夺相同资源造成了线程间的互相等待。这种等待没有外力作用时会陷入循环等待。
用交通状况做比喻,死锁 = 十字路口交叉相行的车把对方的路给堵死了,造成大堵车情况。
1. 死锁产生条件及其检测算法
死锁的发生具备两个条件:
- 互斥条件:多个线程不能同时使用同一个资源。如果线程A正持有资源,那么其它线程必须等待,直到线程A释放该资源。
- 占有式等待条件:即线程B在等待线程A占有的某一资源时,会处于等待状态。并且一直持有着已有的其它资源。
死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
2. 死锁的解决方式
《并发编程的艺术》中给出的解决方法:
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
以上四种其实是预防方法,如果是已发生死锁,可以有以下几种恢复方式: - 利用抢占恢复
- 利用回滚恢复
- 通过杀死进程恢复
3. DeadLock的代码演示
为了便于直观理解,本文给出了一个死锁的演示:线程拿到某资源的锁(可以是数据库锁)之后,因为一些异常情况没有释放锁 (死循环),又或者是释放锁的时候抛出了异常,没释放掉。下面一段代码是结合《深入理解Java虚拟机》与极客时间中给出的死锁场景,是实际开发中可能会遇到的具体问题。
public class NormalLock {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
//1.1尝试获取原始类型int的Class对象,并在方法中给资源加锁
synchronized (Integer.class) {
System.out.println("t1 Integer");
//1.2 获取Long的Class对象并给资源加锁
synchronized (Long.class) {
System.out.println("t1 Long");
}
}
});
threadA.start();//1.3启动线程A
//2.1获取Long的Class对象并给资源加锁
Thread threadB = new Thread(() -> {
synchronized (Long.class) {
System.out.println("t2 Long");
synchronized (Integer.class) {
System.out.println("t2 Integer");
}
}
});
threadB.start();//2.2 启动线程B
}
}
由上述代码中创建了两个资源对象,分别是Int类对象和Long类对象。线程A启动后会先执行代码2.1的Synchronize块试图获取Int类对象这个资源的对象锁,然后执行代码1.2处,尝试获得Long类对象的锁。对于线程B的执行过程亦是如此。
一旦出现死锁,业务是可感知的,因为不能继续提供服务了,那么只能通过dump线程查看 到底是哪个线程出现了问题,以下线程信息告诉我们是DeadLockDemo类的第42行和第31行引起的死锁
方法1: 创建t1和t2两个线程,互相加锁
结果:
方法2: 给t1一定的线程时间
public class DeadLock {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized (Integer.class) {
System.out.println("t1 Integer");
try {
Thread.sleep(1000);//给线程A休眠1s
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Long.class) {
System.out.println("t1 Long");
}
}
});
threadA.start();
Thread threadB = new Thread(() -> {
synchronized (Long.class) {
System.out.println("t2 Long");
synchronized (Integer.class) {
System.out.println("t2 Integer");
}
}
});
threadB.start();
}
}
结果就是一直卡住:
可以看出,要避免一个线程同时获取多个锁。
- 即避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。 ·对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
参考文章:
github—计算机系统之死锁