Java多线程中如何避免死锁

一、什么情况下会出现死锁
假设有两个线程T1,T2。两个账户A,B。刚好某一个时刻在T1线程内A给B转账,在T2线程内B给A转账。代码如下:

 public static void main(String[] args) throws InterruptedException {
        Account a = new Account();
        Account b = new Account();
        Thread t1 = new Thread(() -> {
            a.transfer(b, 5);
        },"t1");
        Thread t2 = new Thread(() -> {
            b.transfer(a, 3);
        },"t2");
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(a.getBalance());
        System.out.println(b.getBalance());
    }
public class Account{
 private int balance;
//转账
public void transfer(Account target,int amount){

    //锁定转出账户
    synchronized (this) { //①
        try {
            System.out.printf("当前线程%s,获得对象锁%s \n",Thread.currentThread().getName(),this.toString());
            Thread.sleep(2000); //等待另一个线程获取另一个对象的锁
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //锁定转入账户
        synchronized (target) { //②
 if (this.balance

对于T1来讲,①处的this指的是A对象,②处target指的是B对象。对于T2来讲,①处的this指的是B对象,②处target指的是A对象。考虑一种情况,当T1线程获取A对象的锁时,T2线程刚好也获取到了B对象的锁(上面代码中的sleep刚好可以造成这种情况),接下来,T1试图获取B对象的锁,但是B对象的锁已经被T2占有,T2则试图获取A对象的锁,A对象的锁又被T1占有了。这样就会造成互相等待,T1,T2都获取不到锁,也不主动释放自己占有的资源。就会形成死锁。

一句话描述死锁:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。

死锁产生的4个必要条件,称为Coffman条件:

  • 互斥,共享资源 X 和 Y 只能被一个线程占用
  • 占有且等待,线程当前持有至少一个资源并请求其他线程持有的其他资源
  • 不可抢占,资源只能由持有它的线程自愿释放,其他线程不可强行占有该资源
  • 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待

二、如何预防出现死锁
根据死锁出现的4个条件,我们只需要破坏其中的任意一个就行。
1.破坏互斥
加锁就是为了互斥,保护资源线程安全。有时可以考虑使用final和Thradlocal,CAS等无锁方式。就是放弃锁。没有锁了,自然也没有死锁了。
2.先看一种简单的方式,破坏第4个条件,循环等待
我们可以给资源一个编号,将这一组资源按一个固定顺序排列,每次获取锁的时候都从编号最小的开始。这样只要涉及到这一组共享资源的获取都是按顺序来的,就保证了一次只有一个线程能获得全部的资源。代码如下:

class Account {
  private int id;
  private int balance;
   //转账
    public void transfer(Account target,int amount){

        Account left = this;
        Account right = target;
        if (this.id > target.id) {
            left = target;
            right = this;
        }

        //锁定转出账户
        synchronized (left) {
            try {
                System.out.printf("当前线程%s,获得对象锁%s \n",Thread.currentThread().getName(),left.toString());
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //锁定转入账户
            synchronized (right) {
     if (this.balance

测试

  public static void main(String[] args) throws InterruptedException {
        Account a = new Account();
        Account b = new Account();
        a.setId(1);
        b.setId(2);
        Thread t1 = new Thread(() -> {
            a.transfer(b, 5);
        },"t1");
        Thread t2 = new Thread(() -> {
            b.transfer(a, 3);
        },"t2");
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(a.getBalance());
        System.out.println(b.getBalance());
    }

结果

当前线程t1,获得对象锁com.xiaoxiao.config.Account@7d6d5cc 
当前线程t1,获得对象锁com.xiaoxiao.config.Account@2afab43e 
当前线程t2,获得对象锁com.xiaoxiao.config.Account@7d6d5cc 
当前线程t2,获得对象锁com.xiaoxiao.config.Account@2afab43e 
8
12

3.破坏占有且等待,线程当前持有至少一个资源并请求其他线程持有的其他资源
这个条件描述的是一个线程持有一个以上资源,并请求其他线程持有的资源。有一种解决方案就是将所有这一类资源的申请放在一起。这样就保证任意一个线程只能不占有任意一个资源或者占有所有的资源。
看代码:


class Allocator {
 private static Allocator allocator = new Allocator();

    private Allocator() {
    }

    public static Allocator getAllocator() {
        return allocator;
    }
  private List als =
    new ArrayList<>();
  // 一次性申请所有资源
  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;
  }
  // 归还资源
  synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
  }
}

class Account {
  // actr应该为单例
  private Allocator actr=Allocator.getAllocator();
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    // 一次性申请转出账户和转入账户,直到成功
    while(!actr.apply(this, target)) //并发时只有一个会成功,如果需要都成功,可以改成while(true) 或者使用等待-通知 (wait()/notify())
      ;
    try{
      // 锁定转出账户
      synchronized(this){              
        // 锁定转入账户
        synchronized(target){           
          if (this.balance > amt){
            this.balance -= amt;
            target.balance += amt;
          }
        }
      }
    } finally {
      actr.free(this, target)
    }
  } 
}
 
 

使用等待-通知机制 优化资源分配器 Allocator
等待通知机制,即当条件不满足时线程进入等待(Blocked 状态),当条件满足时将等待的线程唤醒。这种方式能避免循环等待消耗 CPU 的问题。
由于Allocator是单例模式,this指代的就是这一个对象,所有只需要在apply()方法添加wait(),在free()方法添加notify()

 synchronized void apply(
    Object from, Object to){
    if(als.contains(from) ||
         als.contains(to)){
      try{ wait(); }
catch(Exception e){ }  
    } else {
      als.add(from);
      als.add(to);  
    }
    return true;
  }
  // 归还资源
  synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
    notifyAll();
  }

4、破坏不可抢占,资源只能由持有它的线程自愿释放,其他线程不可强行占有该资源
synchronized 关键字不能做到自动释放资源,在java里面可以使用Lock里面的方法tryLock()
下面是一个例子:

class Account {
    private int balance = 10;
    private final Lock lock = new ReentrantLock();
    //转账
    public void transfer(Account target, int amount) throws InterruptedException {
        Random random = new Random();
        while (true) {
            //锁定转出账户
            if (this.lock.tryLock(random.nextInt(1000), TimeUnit.NANOSECONDS)) {
                try {
                    System.out.printf("当前线程%s,获得对象锁%s \n", Thread.currentThread().getName(), this.toString());
                    //锁定转入账户
                    if (target.lock.tryLock() ){
                        try {
                            System.out.printf("当前线程%s,获得对象锁%s \n", Thread.currentThread().getName(), target.toString());
                            if (this.balance < amount) {
                                return;
                            }
                            this.balance = balance - amount;
                            target.balance = target.balance + amount;
                            break;
                        } finally {
                            target.lock.unlock();
                        }
                    }
                } finally {
                    System.out.println("释放锁");
                    this.lock.unlock();
                }

            }
        }
    }

    public int getBalance() {
        return balance;
    }

}

tryLock()方法在获取不到资源的时候会释放已经获取的资源,这样就不会形成死锁。需要注意的是只用tryLock(),会有活锁的问题,当T1获取A对象,T2获取B对象时,T1尝试获取B对象,T2尝试获取A对象都不会成功,就造成一直在取消-重试。这就是活锁。通过在第一次获取锁时加上一个随机超时时间来减少重试次数,解决活锁的问题。运行结果

...
当前线程t1,获得对象锁deadlock3.Account@7287a92e 
当前线程t1,获得对象锁deadlock3.Account@6e22e576 
当前线程t2,获得对象锁deadlock3.Account@6e22e576 
释放锁
当前线程t2,获得对象锁deadlock3.Account@6e22e576 
释放锁
当前线程t2,获得对象锁deadlock3.Account@6e22e576 
释放锁
当前线程t2,获得对象锁deadlock3.Account@6e22e576 
释放锁
当前线程t2,获得对象锁deadlock3.Account@6e22e576 
释放锁
释放锁
当前线程t2,获得对象锁deadlock3.Account@6e22e576 
当前线程t2,获得对象锁deadlock3.Account@7287a92e 
释放锁
8
12

你可能感兴趣的:(Java多线程中如何避免死锁)