什么是死锁
先通过一段产生死锁的代码来理解死锁是怎么产生的。
/**
* 线程死锁
*
*/
public class ThreadDeadkockStudy {
// 钱
static Object money = new Object();
// 货
static Object goods = new Object();
public static void main(String[] args) {
// 卖家
new Thread(new Runnable() {
public void run() {
// 卖家拿着货
synchronized (goods) {
System.out.println("卖家:先钱!");
try {
// 我看你怎么说
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 除非你先给钱
synchronized (money) {
System.out.println("卖家:合作愉快");
}
}
}
}).start();
// 买家
new Thread(new Runnable() {
public void run() {
// 买家拿着钱
synchronized (money) {
System.out.println("买家:不行,我先验验货!");
try {
// 我也看你怎么说
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 除非你先给我验验货
synchronized (goods) {
System.out.println("买家:合作愉快");
}
}
}
}).start();
}
}
最后输出的结果是这样的,我们发现一直卡在这里,没有继续往下执行了。
卖家:先钱!
买家:不行,我先验验货!
再用比较通俗的语言去解释这个现象。
黑社会和商人一起到达了约定的交易地点。
商人:“先钱!”
黑社会:“不行,我先验验货!”
商人看不到黑社会的钱,不愿意把手里的货交出去;黑社会怕商人给的假货,也不把钱拿出来,就这样僵住了。
两个不同的线程,分别拥有各自的资源,并且都想拥有对方持有的资源,但是谁也不愿意先妥协,最后陷入了僵局,也就是我们所说的死锁。
死锁触发条件
我们继续看下面的几组代码,看看为什么他们为什么代码类似,但是没有产生死锁呢。
卖家强买强卖
// 卖家强买强卖
public static void sellerFast() {
// 卖家
new Thread(new Runnable() {
public void run() {
synchronized (goods) {
System.out.println("卖家:先钱!");
synchronized (money) {
System.out.println("卖家:合作愉快");
}
}
}
}).start();
// 买家
new Thread(new Runnable() {
public void run() {
synchronized (money) {
System.out.println("买家:不行,我先验验货!");
synchronized (goods) {
System.out.println("买家:合作愉快");
}
}
}
}).start();
}
再看看输出的结果,由于卖家没有等待买家的回应,就把买家兜里的钱给拿走了,并且把货也硬塞到了自己的兜里,只能吃哑巴亏了。
卖家:先钱!
卖家:合作愉快
买家:不行,我先验验货!
买家:合作愉快
这个产生的原因是,我们线程初始化和启动是需要一点时间的,而第一个线程已经执行完了,第二个线程才刚刚执行到那里的时候,这个锁已经被释放了。
如果两个线程执行的先后顺序对调的情况,我们就可以理解成是:买家看卖家不在店里,但是看到了自己想买的东西,没等卖家回来就把钱放在了柜台上,并且留下了个字条:我看你不在,我把货拿走了,钱我放在柜台了!
最后他们成功的完成了这笔交易。
- 互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放 。
- 请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
- 不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用。
- 循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
我们这里没有满足第二条,因为当我们想要取用该资源的时候,资源已经被提早释放了,所以就没有造成死锁。
如何避免死锁
在生活中,一般造成死锁都是由于沟通问题导致的,比如跨行转账也可能出现死锁的情况,但是这完全是可以被避免的。
如果没有中间机构,我们也可以通过自行妥协的方式,规定一个比较合理的顺序,来避免死锁的诞生。
加锁顺序: 当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。当然这种方式需要你事先知道所有可能会用到的锁,然而总有些时候是无法预知的。
加锁时限: 加上一个超时时间,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。但是如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。
死锁检测: 死锁检测即每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
参考资料
文章中出现的任何错误欢迎指正,共同进步!
最后做个小小广告,有对WEB开发和网络安全感兴趣的,可以加群一起学习和交流!
QQ:425343603