美文网首页
4-4 如何避免死锁

4-4 如何避免死锁

作者: nieniemin | 来源:发表于2021-08-14 18:54 被阅读0次

我们先来看下到底都有哪些原因造成了死锁,Coffman大佬总结了只有以下这四个条件都发生时才会出现死锁:

  1. 互斥,共享资源 X 和 Y 只能被一个线程占用;

  2. 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;

  3. 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;

  4. 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

知道了死锁产生的必要条件后,如何去避免呢?

对于第一条,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥;不过其他三个条件都是有办法破坏掉的:

  1. 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。

  2. 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

  3. 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。

理论上我们已经知道怎么避免死锁了,接下来通过代码来加深一下印象

1. 破坏占用且等待条件

对于转账例子来说,它需要的资源有两个,一个是转出账户,另一个是转入账户。如何一次性申请所有的资源(转出、转入账户呢)?

我们需要引入一个管理员(Manager)的角色,统一管理这两个资源。他的作用是当执行转账操作时,首先去管理员这里申请全部的资源,成功后再锁定这两个资源,当转账完成之后,通知管理员立刻释放资源。


统一申请资源
public class Account {
 //账号
 private String accountName;
 // 余额
 private int balance;
 public Account(String accountName,int balance){
 this.accountName = accountName;
 this.balance = balance;
 }

// 省略get/set

}
/**
 * <p>必须是单例的,只能有一个人分配资源</p>
 *
 * @version 1.0
 * @date : 2021/8/14 17:40
 */
public class Manager {
    private volatile static Manager manger;
    private Manager(){}

    public static Manager getInstance() {
        if (manger == null) {
            synchronized (Manager.class) {
                if (manger == null) {
                    manger = new Manager();
                }
            }
        }

        return manger;
    }
    private Set<Object> lockResources = new HashSet<Object>();

    public synchronized boolean apply(Object... objs) {
        for (Object obj : objs) {
            if (lockResources.contains(obj)) {
                return false;
            }
        }
        for (Object obj : objs) {
            lockResources.add(obj);
        }

        return true;
    }

    public synchronized void free(Object... objs) {
        for (Object obj : objs) {
            if (lockResources.contains(obj)) {
                lockResources.remove(obj);
            }
        }
    }

}
public class AccountUnLock implements Runnable {
    //转出账户
    public Account fromAccount;
    //转入账户
    public Account toAccount;
    //转出金额
    public int amount;

    public AccountUnLock(Account fromAccount, Account toAccount, int amount){
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
    }
    @Override
    public void run(){

        while(true) {
            //1.申请资源时,先去管理员获取到所有资源。
            Manager instance = Manager.getInstance();
            try {
                while(!instance.apply(fromAccount, toAccount));
                // 2.给资源加锁
                synchronized (fromAccount) {
                    synchronized (toAccount) {
                        //转账进行的条件:判断转出账户的余额是否大于0
                        if(fromAccount.getBalance() <= 0){
                            System.out.println(fromAccount.getAccountName() + "账户余额不足!");
                            return;
                        }else{
                            //更新转出账户的余额:
                            fromAccount.setBalance(fromAccount.getBalance() - amount);
                            //更新转入账户的余额:+
                            toAccount.setBalance(toAccount.getBalance() + amount);

                        }
                        System.out.println("转出用户:" + fromAccount.getAccountName() + "余额:" + fromAccount.getBalance());
                        System.out.println("转入用户:" +toAccount.getAccountName() + "余额:" + toAccount.getBalance());
                    }

                }// 3.释放锁
            } finally {
                // 4.通知管理员释放资源.
                instance.free(fromAccount, toAccount);

            }


        }
    }

    public static void main(String[] args) {
        Account fromAccount = new Account("张三",100000);
        Account toAccount = new Account("李四",100000);

        Thread a = new Thread(new AccountUnLock(fromAccount,toAccount,1));
        Thread b = new Thread(new AccountUnLock(toAccount,fromAccount,1));

        a.start();
        b.start();

    }

}

2.破坏不可抢占条件

破坏不可抢占核心是要能够主动释放它占有的资源,这一点 synchronized 是做不到的。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的。

public class AccountUnLockReentrant implements Runnable {
    //转出账户
    public Account fromAccount;
    //转入账户
    public Account toAccount;
    //转出金额
    public int amount;
    private Lock lock = new ReentrantLock();

    public AccountUnLockReentrant(Account fromAccount, Account toAccount, int amount) {
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
    }

    @Override
    public void run() {

        while (true) {
            try {
                // 锁【lock.lock】必须紧跟try代码块,且unlock要放到finally第一行
                lock.lock();
                
                //转账进行的条件:判断转出账户的余额是否大于0
                if (fromAccount.getBalance() <= 0) {
                    System.out.println(fromAccount.getAccountName() + "账户余额不足!");
                    return;
                } else {
                    //更新转出账户的余额:
                    fromAccount.setBalance(fromAccount.getBalance() - amount);
                    //更新转入账户的余额:+
                    toAccount.setBalance(toAccount.getBalance() + amount);

                }
                System.out.println("转出用户:" + fromAccount.getAccountName() + "余额:" + fromAccount.getBalance());
                System.out.println("转入用户:" + toAccount.getAccountName() + "余额:" + toAccount.getBalance());

            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Account fromAccount = new Account("张三", 100000);
        Account toAccount = new Account("李四", 100000);

        Thread a = new Thread(new AccountUnLockReentrant(fromAccount, toAccount, 1));
        Thread b = new Thread(new AccountUnLockReentrant(toAccount, fromAccount, 1));

        a.start();
        b.start();

    }

}

3.破坏循环等待条件

破坏这个条件,需要对资源进行排序,然后按序申请资源。申请的时候,我们可以按照从小到大的顺序来申请。这样就不存在“循环”等待了。

public class AccountMain implements Runnable {
    //转出账户
    public Account fromAccount;
    //转入账户
    public Account toAccount;
    //转出金额
    public int amount;

    public AccountMain(Account fromAccount, Account toAccount, int amount){
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
    }
    @Override
    public void run(){
        Account left =null;
        Account right = null;
        if (fromAccount.hashCode() > toAccount.hashCode()){
            left = toAccount;
            right =fromAccount;
        }
        while(true){
            synchronized (left) {
                synchronized (right) {
                    //转账进行的条件:判断转出账户的余额是否大于0
                    if(fromAccount.getBalance() <= 0){
                        System.out.println(fromAccount.getAccountName() + "账户余额不足,无法进行转账");
                        return;
                    }else{
                        //更新转出账户的余额:
                        fromAccount.setBalance(fromAccount.getBalance() - amount);
                        //更新转入账户的余额:+
                        toAccount.setBalance(toAccount.getBalance() + amount);
                    }
                }

            }

            System.out.println("转出用户:" + fromAccount.getAccountName() + "余额:" + fromAccount.getBalance());
            System.out.println("转入用户:" +toAccount.getAccountName() + "余额:" + toAccount.getBalance());
        }
    }

    public static void main(String[] args) {
        Account fromAccount = new Account("张三",100000);
        Account toAccount = new Account("李四",200000);

        Thread a = new Thread(new AccountMain(fromAccount,toAccount,1));
        Thread b = new Thread(new AccountMain(toAccount,fromAccount,1));

        a.start();
        b.start();
    }

}

4.优化循环等待

  while(!instance.apply(fromAccount, toAccount));

在破坏占用且等待条件代码中,使用while自旋的方式调用apply方法来统一管理资源,如果 apply() 操作耗时非常短,而且并发冲突量也不大时,这个方案还挺不错的,因为这种场景下,循环上几次或者几十次就能一次性获取转出账户和转入账户了。但是如果 apply() 操作耗时长,或者并发冲突量大的时候,循环等待这种方案就不适用了,因为在这种场景下,可能要循环上万次才能获取到锁,太消耗 CPU 了。
我们用等待-通知来优化一下。当线程要求的条件不满足,则线程阻塞自己,进入等待状态;当线程要求的条件满足后,通知等待的线程重新执行。

public class Allocator {
    private volatile static Allocator manger;
    private Allocator(){}

    public static Allocator getInstance() {
        if (manger == null) {
            synchronized (Allocator.class) {
                if (manger == null) {
                    manger = new Allocator();
                }
            }
        }

        return manger;
    }
    private Set<Object> lockResources = new HashSet<Object>();

    public synchronized void apply(Object... objs) {
        for (Object obj : objs) {
            while (lockResources.contains(obj)) {

                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        for (Object obj : objs) {
            lockResources.add(obj);
        }
    }

    public synchronized void free(Object... objs) {
        for (Object obj : objs) {
            if (lockResources.contains(obj)) {
                lockResources.remove(obj);
            }
        }
        notifyAll();
    }

}
public class AccountUnLock2 implements Runnable {
    //转出账户
    public Account fromAccount;
    //转入账户
    public Account toAccount;
    //转出金额
    public int amount;

    public AccountUnLock2(Account fromAccount, Account toAccount, int amount){
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
    }
    @Override
    public void run(){

        while(true) {
            //1.申请资源时,先去管理员获取到所有资源。
            Allocator instance = Allocator.getInstance();
            try {
                instance.apply(fromAccount, toAccount);
                //转账进行的条件:判断转出账户的余额是否大于0
                if(fromAccount.getBalance() <= 0){
                    System.out.println(fromAccount.getAccountName() + "账户余额不足!");
                    return;
                }else{
                    //更新转出账户的余额:
                    fromAccount.setBalance(fromAccount.getBalance() - amount);
                    //更新转入账户的余额:+
                    toAccount.setBalance(toAccount.getBalance() + amount);

                }
                System.out.println("转出用户:" + fromAccount.getAccountName() + "余额:" + fromAccount.getBalance());
                System.out.println("转入用户:" +toAccount.getAccountName() + "余额:" + toAccount.getBalance());
            } finally {
                // 4.通知管理员释放资源.
                instance.free(fromAccount, toAccount);

            }


        }
    }

    public static void main(String[] args) {
        Account fromAccount = new Account("张三",100000);
        Account toAccount = new Account("李四",100000);

        Thread a = new Thread(new AccountUnLock2(fromAccount,toAccount,1));
        Thread b = new Thread(new AccountUnLock2(toAccount,fromAccount,1));

        a.start();
        b.start();

    }

}

线程间通信wait/notify

总结

在这一节中介绍了产生死锁的四个原因,以及如何避免死锁。通过等待-通知的方式优化循环机制。

相关文章

  • 4-4 如何避免死锁

    我们先来看下到底都有哪些原因造成了死锁,Coffman大佬总结了只有以下这四个条件都发生时才会出现死锁: 互斥,共...

  • JavaEE面试题总结 Day39 2018-12-29

    什么是线程死锁?死锁如何产生?如何避免线程死锁? 死锁的介绍: 线程死锁是指由于两个或者多个线程互相持有对方所需要...

  • java并发--java死锁

    本篇结构: 前言 什么是死锁 产生死锁的必要条件 死锁的代码示例 死锁排查 如何避免死锁 总结 一、前言 今天被问...

  • 死锁

    线程饥饿死锁 锁顺序死锁 动态锁顺序死锁通过锁顺序来避免死锁 避免死锁

  • jstack命令:教你如何排查多线程问题

    这是之前的一个死锁案例: 一个多线程死锁案例,如何避免及解决死锁问题? 如程序中发生这样的死锁问题该如何排查呢?我...

  • Java死锁

    什么是死锁 死锁检测 产生死锁的四个必要条件 如何避免死锁 死锁 死锁,指两个或多个线程之间,由于互相持有对方需要...

  • 如何避免死锁

    死琐 A线程持有a锁,等待获取b锁;同时B线程持有b锁,等待获取a锁。 死琐条件 -- 独占锁: 互斥:资源不能被...

  • 如何避免死锁

    避免死锁主要有 3种方式 加锁顺序 加锁时限 死锁检测 加锁顺序 一个线程如果要获取多个锁,必须按照一定的顺序去获...

  • 线程死锁,死锁条件,如何避免死锁

    死锁:有两个或两个以上线程相互持有对方所需要的资源,而使得这些线程无法往下执行下去。在Java中程序执行进入对象的...

  • 如何快速排查死锁?如何避免死锁?

    前言 相信程序员都会碰上这样的问题,Java死锁如何排查?又如何解决呢?那么,何为死锁呢?死锁是指两个或两个以上的...

网友评论

      本文标题:4-4 如何避免死锁

      本文链接:https://www.haomeiwen.com/subject/xcdebltx.html