目录
死锁是什么?
官方定义:死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
介绍的是不是很抽象?那到底什么是死锁呢?我们来看一个生活中的例子:
现有两个仓库,仓库A:存放苹果手机;仓库B:存放华为手机,每天下午4点中都会有工作人员来仓库查看设备情况。
用户A来到仓库A,用户B来到仓库B,并且他们都各自拿到了各自仓库的钥匙,用户A进入仓库A之后进行检查,发现有一台huawei p10放在了本应该存放iiphone的仓库,用户B进入仓库进行检查发现有一台iphone 8p放在了存放华为的仓库,这时候用户A与用户B都要将放错的手机送还到对应的仓库中,用户A拿着huawei p10 来到仓库B管理处拿取仓库B的钥匙,需要将huawei p10放入仓库B,用户B拿着iphone 8p来到仓库A管理处拿仓库A的钥匙,需要将iphone 8p放入仓库A,由于仓库A的钥匙被用户A拿着,仓库B的钥匙被用户B拿着,所以他们都拿不到对方的钥匙,这时候他们就会在管理处等待钥匙被送回来,用户A等着用仓库B的钥匙被送回来(用户B拿着仓库B的钥匙),用户B等着仓库A的钥匙被送回来(用户A拿着仓库A的钥匙),很明显不会被送回来,因为他们都在对方的管理处等着对方的钥匙被送回来,由于这是显示世界,如果长时间没有人送回钥匙,那么可以通过电话的方式沟通,然后拿到钥匙,但是程序做不到人的那么智能,他会一直等下去,也就是死等,换到程序世界,就是死锁。
注意:这里不考虑用户拿出手机后先放回钥匙再去对方仓库,否者条件不成立。
那我们再举一个程序世界的例子,方便理解,也是后面实现死锁的例子:
现在有两个账户:账户A、账户B;
需求:
1.同一时刻。
2.A向B转入100元,B向A转入50元(金额随意)。
3.对各自账户加入互斥锁,如:synchronized。
A-->B转账大致流程:
1.获取账户A的锁(保证此时其他对A账户进行修改的操作处于阻塞状态)。
2.获取账户B的锁(保证此时其他对B账户进行修改的操作处于阻塞状态)。
3.做逻辑判断,如:金额是否足够、金额是否冻结等等。
4.A账户扣钱。
5.B账户加钱。
A-->B转账大致流程:
1.获取账户B的锁(保证此时其他对B账户进行修改的操作处于阻塞状态)。
2.获取账户A的锁(保证此时其他对A账户进行修改的操作处于阻塞状态)。
3.做逻辑判断,如:金额是否足够、金额是否冻结等等。
4.B账户扣钱。
5.A账户加钱。
有两个线程分别去执行上面两个操作,线程1(执行A--->B转账);线程2(执行B--->A转账),如果如果线程1执行A--->B转账)执行完第一步(获取到了账户A的锁),此时线程切换到线程2执行B--->A转账),执行了第一步(获取到了账户B的锁),这时候线程再被切换到线程1(执行A--->B转账),执行第二步的发现获取不到账户B的锁(已被线程2(执行B--->A转账)获取),这时候线程1(执行A--->B转账)就会处于阻塞状态,等待账户B的锁被释放,切换到线程2(执行B--->A转账),执行第二步:获取账户A的锁(已被线程1(执行A--->B转账)获取),此时线程2(执行B--->A转账)也会处于阻塞状态,就这样,他们就会一直阻塞下去,形成了死锁,除非程序重启,否者无法解除这个死锁。
注:这里的锁使用的是不可中断的synchronized,否者条件不成立
产生死锁的条件
4个条件,缺一不可。
1.互斥条件:指进程(线程)对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程(线程)占用。如果此时还有其它进程(线程)请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
如:仓库只能进去一个人,如果第二个人想要进去,必须要等待前一个人出来之后才可以
2.请求和保持条件:指进程(线程)已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程(线程)占有,此时请求进程(线程)阻塞,但又对自己已获得的其它资源保持不放。
如:用户A拿到了仓库A的钥匙之后再去拿仓库B的钥匙,同时不能将仓库A的钥匙送回管理处。
3.不剥夺条件:指进程(线程)已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
如:用户进入仓库之后需要清点手机数量与型号,不能在还没有完成时被强制带走,只能用户自己清点完成之后出来并归还钥匙。
4.环路等待条件:指在发生死锁时,必然存在一个进程(线程)——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
如:上面的仓库A与仓库B的例子就是这个条件
用代码重现死锁
private static Account accountA = new Account();
private static Account accountB = new Account();
static {
accountA.setId(1);
accountA.setBalance(BigDecimal.valueOf(100));
accountB.setId(2);
accountB.setBalance(BigDecimal.valueOf(200));
}
/**
* 转账
*/
private static void transfer(BigDecimal num,Account account1, Account account2) {
synchronized (account1) {
Account a1 =account1;
Account a2 = null;
try {
//休眠1秒的作用是为了能让线程1、2获取到自己账户锁的同时还没有获取到对方账户的锁,以达成死锁条件
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (account2) {
a2 = account2;
}
if (a1.getBalance().compareTo(num) < 0) {
System.out.println("余额不足,转出失败");
}
a1.setBalance(a1.getBalance().subtract(num));
a2.setBalance(a2.getBalance().add(num));
System.out.println("转出账户:" + a1);
System.out.println("转入账户:" + a2);
}
}
public static void main(String[] args) {
//线程1
new Thread(() -> {
transfer(BigDecimal.valueOf(10), accountA, accountB);
}).start();
//线程2
new Thread(() -> {
transfer(BigDecimal.valueOf(5), accountB, accountA);
}).start();
}
如何查看这段代码是否发生了死锁?
1.查看输出结果:如果没有输出账户A、B的信息,表示可能死锁了。
2.查看jvm,死锁会有提示。
如何查看jvm呢?这里我介绍一个idea的插件:VisualVM Launcher
安装完成之后重启idea,你会发现在启动程序的地方多了两个按钮:
使用这两个按钮启动程序,就可以查看jvm的情况了,我们来启动看看效果:
查看线程Dump:
发现线程t1与线程t0都处于阻塞状态。
如何避免死锁
我们先来看一下产生死锁的条件:
1.互斥条件
2.请求和保持条件
3.不剥夺条件
4.环路等待条件
缺一不可,也就是说,只需要破坏其中一条,死锁也就不成立了,通过上面对思索条件的介绍,再结合线程安全的条件,前三个条件无法被破坏,那么我们只要破坏第四种情况即可,下面来介绍几种防止死锁的方案。
1.使用可中断的锁(Lock):
当线程1、2都获取到了自己的锁,同时需要索取对方锁的时候,lock提供了tryLoc(),如果获取锁失败或者再规定的时间内有获取到锁,那么你可以手动中断此操作,这样即可防止死锁。
2.一次性获取到所有的资源(锁):可以写一个while循环,在循环内去获取两个账户的锁(这个方法加synchronized,保证互斥),保证两个账户锁都没有被获取的情况下标记一下,然后退出循环,对两个账户加锁,后面的while循环,由于读取到那个标记都会一直处于循环等待状态,锁释放成功之后修改标记,让下一个线程获取锁,这样也能防止死锁。
3.对锁排序获取:这个比较抽象,就是对要获取的锁进行一下降序或者升序,然后从小到大或者从大到小依次获取锁,这样同样可以防止死锁。
总结
防止死锁的方式还有很多,这里我只是简单的介绍了三种,再并发编程中,死锁是比较容易碰到的问题,如何避免死锁以及如何再避免死锁的情况下提升系统的性能都是程序猿需要深入研究的东西,希望本片文章能帮助你对死锁有所了解。