死锁的来龙去脉

前言引入

之前有提到过一把锁保护多个资源,如果是多个资源间存在关联关系,如账户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时银行柜台工作人员大概有如下三种情况:

  1. 账户A账本不在柜台,那么银行柜台人员等待账本A归还。
  2. 账本A存在柜台中,柜台人员去获取账本B时账本B不在柜台中,柜台人员等待账本B归还。
  3. 账本A、B都存在柜台中,柜员都可以获取到。

这时存在两个线程T1,T2去完成转账操作,T1需要将账户A的100元转账给账户B,T2需要将账户B的150元转账给账户A,T1获取到了账户A的账本,即将去获取账户B的账本时,T2获取到账户B的账本,同时想去获取账户A的账本,如下图所示。

如何找到死锁的java进程_死锁

这时候T1,T2都陷入等待状态,都想获取对方手里的账本,这就是常说的”死等“,这里也就是编程时间常说的死锁。

死锁定义

什么是死锁呢?

故多个互相竞争资源的线程相互等待,导致永久阻塞的现象。

需要注意的是,死锁一旦产生没有很好的解决办法,只能重启应用解决,所以解决死锁最好的办法就是避免死锁。

如何避免死锁

要想学会避免死锁,就需要弄清楚死锁发生的必要条件,死锁发生就必须同时满足如下四个条件。

  1. 互斥,同一时刻只能有一个线程访问。
  2. 持有且等待,当线程持有资源A时,再去竞争资源B并不会释放资源A。
  3. 不可抢占,线程T1占有资源A,其他线程不能强制抢占。
  4. 循环等待,线程T1占有资源A,再去抢占资源B如果没有抢占到会一直等待下去。

想要破坏死锁那么上诉条件只要不满足一个即可,那么分析如下

  1. 互斥条件,不可破坏,如果破坏那么并发安全就不存在了。
  2. 持有且等待,可以破坏,可以一次性申请所有的资源。
  3. 不可抢占,当线程T1持有资源A再次获取资源B时,发现资源B被占用那么主动释放资源A。
  4. 循环等待,可以将资源排序,可以按照排序顺序的资源申请,这样就不会存在环形资源申请了。

破坏持有且等待

想要一次性申请所有的资源,那么所有的资源管理由管理员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;
            }
        }
    }
}

破坏死锁的方法有很多,对于目前的场景而言当然是破坏循环等待条件来的最为快捷和方便,在开发过程中需要衡量各个方法间的合理性。