作者:禹明明 叩丁狼高级讲师。原创文章,转载请注明出处。
线程死锁示例
为了维护数据的一致性,多线程环境下必须对一些方法进行加锁,但是如果锁资源使用不当也会带来一些隐患,比如死锁。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
先来看一下死锁产生的四个必要条件。
- 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,⋯⋯,Pn正在等待已被P0占用的资源。即肯定存在相互等待的死循环
那么基于以上四个条件我们可以设计一个小小的示例来理解一下死锁是怎么产生的
首先定义出两个锁对象lock1, lock2, 这里我就直接把锁对象放在了测试类中
PS:在java 中任何的引用数据类型对象都可以当做锁对象,我们可以简单的理解为锁对象其实就是一个象征性的物品,就好比国王的权杖,想当国王的人必须拿到权杖,但是权杖本身可能是任何东西做成的,并没有特殊要求,它是大家约定好一致认可的象征. 锁对象也是一样,是大家约定的一个象征,只有拿到它的才有资格进入它所锁定的方法体。
但是有一点要注意的就是,最好不要使用String , Integer , Boolean , Character Short 这些有缓存机制的对象作为锁对象,因为有常量池和缓存的存在很可能导致你的其他不相干的同步方法使用的是同一个锁对象严重影响程序运行效率
public class TestThread {
public static final Object lock1 = new Object();//锁对象1
public static final Object lock2 = new Object();//锁对象2
//测试方法
public static void main(String[] args) {
new A("A").start();
new B("B").start();
}
}
然后定义两个线程类 A , B
其中线程A运行时会先获取lock1锁对象,然后在不释放lock1的情况下去争夺lock2
而线程B运行时会先获取lock2锁对象,然后在不释放lock2的情况下去争夺lock1
这时就有可能会出现这么一种情况:线程A获取到了lock1,准备去抢lock2,线程B获取到了lock2,准备去抢lock1
但是此时他们所需要的资源已经被对方所占有而且拥有不会主动释放,程序就会一直等待下去,死锁就产生了
class A extends Thread {
public A(String name) {
super(name);
}
@Override
public void run() {
//先获取锁对象lock1,在不释放锁lock1的情况下去获取lock2
synchronized (TestThread.lock1) {
System.out.println(Thread.currentThread().getName()
+ "已获取到lock1, 准备获取lock2");
//使用Thread.sleep可以使线程并发问题更明显,sleep方法调用时并不会释放锁对象
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (TestThread.lock2) {
System.out.println(Thread.currentThread().getName()+ "已完整获取到lock1, lock2");
}
}
}
}
class B extends Thread {
public B(String name) {
super(name);
}
@Override
public void run() {
//先获取锁对象lock2,在不释放锁lock2的情况下去获取lock1
synchronized (TestThread.lock2) {
System.out.println(Thread.currentThread().getName()+ "已获取到lock2, 准备获取lock1");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (TestThread.lock1) {
System.out.println(Thread.currentThread().getName()+ "已完整获取到lock1, lock2");
}
}
}
}
打印结果:
A已获取到lock1, 准备获取lock2
B已获取到lock2, 准备获取lock1
并且可以看到进程已经发生死锁,本该结束的方法一直卡着不会结束。
死锁是在开发过程中必须有极力避免的,那么怎么处理呢?
我们可以从三个方面着手:
死锁预防
- 有序资源访问
对于资源的申请必须按照统一的顺序进行申请,例如现有资源A 、B 、C那么所有需要申请此资源的线程都必须按照ABC的顺序进行申请,没有申请到A之前不能去申请B或C, 这样就可以打破死锁四大条件的环路等待从而避免死锁 - 银行家算法 https://baike.baidu.com/item/%E9%93%B6%E8%A1%8C%E5%AE%B6%E7%AE%97%E6%B3%95/1679781?fr=aladdin
死锁避免
系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源;如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入死锁状态的动态策略。但并不容易实现
死锁检测和排除
此方法允许系统在运行过程中发生死锁。但可通过系统所设置的检测机制,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源。(检测方法包括定时检测、效率低时检测、进程等待时检测等)。然后根据相关信息清除死锁,检测和清除死锁的方式请参考我的其他几篇文章。