死锁的来龙去脉
前言引入
之前有提到过一把锁保护多个资源,如果是多个资源间存在关联关系,如账户A给账户B转账,一把锁怎么锁住同一个资源呢?当时只是采用简单的方法,锁住整个类模板Account.class方法解决,如下代码。
public class Account {
private Integer balance;
// 新增代码结束
public void transfer( Account target, int amt){
// 获取锁 使用类模板
synchronized (Account.class){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
这时候并发安全性问题解决了,但是因为锁的是账户类Account.class模板类,相当于锁住了所有的账户,能不能想着将锁细粒度化呢?当然是可以的,如下代码
public class Account {
private Integer balance;
public void transfer( Account target, int amt){
// 获取锁
synchronized (this){
synchronized (target){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
这时候将锁细粒度化为锁住this对象和target对象,不再锁住Account,class类模板,当然效率将会大幅度提升,不过需要注意万事有利有弊,这时引进一个新问题,结合实际生活问题说明。
生活场景
假如生活中的银行转账全部只能线下完成,每个账户就是一个账本,当账户A转账给账户B时银行柜台工作人员大概有如下三种情况:
- 账户A账本不在柜台,那么银行柜台人员等待账本A归还。
- 账本A存在柜台中,柜台人员去获取账本B时账本B不在柜台中,柜台人员等待账本B归还。
- 账本A、B都存在柜台中,柜员都可以获取到。
这时存在两个线程T1,T2去完成转账操作,T1需要将账户A的100元转账给账户B,T2需要将账户B的150元转账给账户A,T1获取到了账户A的账本,即将去获取账户B的账本时,T2获取到账户B的账本,同时想去获取账户A的账本,如下图所示。
这时候T1,T2都陷入等待状态,都想获取对方手里的账本,这就是常说的”死等“,这里也就是编程时间常说的死锁。
死锁定义
什么是死锁呢?
故多个互相竞争资源的线程相互等待,导致永久阻塞的现象。
需要注意的是,死锁一旦产生没有很好的解决办法,只能重启应用解决,所以解决死锁最好的办法就是避免死锁。
如何避免死锁
要想学会避免死锁,就需要弄清楚死锁发生的必要条件,死锁发生就必须同时满足如下四个条件。
- 互斥,同一时刻只能有一个线程访问。
- 持有且等待,当线程持有资源A时,再去竞争资源B并不会释放资源A。
- 不可抢占,线程T1占有资源A,其他线程不能强制抢占。
- 循环等待,线程T1占有资源A,再去抢占资源B如果没有抢占到会一直等待下去。
想要破坏死锁那么上诉条件只要不满足一个即可,那么分析如下
- 互斥条件,不可破坏,如果破坏那么并发安全就不存在了。
- 持有且等待,可以破坏,可以一次性申请所有的资源。
- 不可抢占,当线程T1持有资源A再次获取资源B时,发现资源B被占用那么主动释放资源A。
- 循环等待,可以将资源排序,可以按照排序顺序的资源申请,这样就不会存在环形资源申请了。
破坏持有且等待
想要一次性申请所有的资源,那么所有的资源管理由管理员Allocator负责,当然Allocator必须是单例的,这样才能保证所有的容器als是同一个。
package com.example.demo.lock;
import java.util.ArrayList;
import java.util.List;
public class Account {
private Allocator allocator = Allocator.getAllocator();
private Integer balance;
public void transfer( Account target, int amt){
// 申请资源,轮询等待
while (!allocator.apply(this,target));
try {
synchronized (this){
synchronized (target){
if (this.balance >= amt){
this.balance-=amt;
target.balance+=amt;
}
}
}
}finally {
// 释放资源
allocator.free(this,target);
}
}
}
// 管理员类
class Allocator{
private static Allocator allocator = null;
private Allocator(){
}
// 资源容器
private List<Object> als = new ArrayList<>();
// 申请资源
public synchronized Boolean apply(Object from,Object to){
// 只要包含一个账户申请失败
if (als.contains(from) || als.contains(to)){
return false;
}else {
als.add(from);
als.add(to);
}
return true;
}
// 释放资源
public synchronized void free(Object from,Object to){
als.remove(from);
als.remove(to);
}
// 简单单例
public static Allocator getAllocator(){
if (allocator == null){
synchronized (Allocator.class){
if (allocator == null){
allocator = new Allocator();
}
}
}
return allocator;
}
}
破坏不可抢占
想要破坏不可抢占那么需要线程主动释放资源,但是对于管程原语synchronized来讲并没有这个功能,如果抢占不到资源synchronized只会进入阻塞状态,这时候可以采用JUC中的Lock类中的tryLock(long, TimeUnit) 方法,就可以完美解决。
破坏循环等待
破坏循环等待相对简单就是给资源排序,所有的线程按照资源顺序获取资源,这样不会形成环状请求,避免死锁,代码示例如下。
package com.example.demo.lock;
// 破坏循环等待
public class AccountTest {
private int id;
private int balance;
// 转账业务
public void transfer(AccountTest target,int amt){
AccountTest left = target;
AccountTest right = this;
if (this.id > target.id){
left = this;
right = target;
}
// 先加锁id大的
synchronized (left){
synchronized (right){
this.balance-=amt;
target.balance+=amt;
}
}
}
}
破坏死锁的方法有很多,对于目前的场景而言当然是破坏循环等待条件来的最为快捷和方便,在开发过程中需要衡量各个方法间的合理性。