• Java的死锁及解决思路(延伸: 活锁,饥饿,无锁)


    死锁

    A线程持有 锁1,接下来要获取锁2;与此同时,B线程持有锁2,要获取锁1。两个线程都在等对方释放自己需要的锁,这时两方会永远等待下去,就形成了死锁。

    死锁的四个必要条件:

    1.互斥:资源(锁)同时只能被一个线程占用。

    2.占有且等待:线程已经占用资源A,同时等待资源B时,不释放资源A。

    3.不可抢占:其他线程不能强行获取当前线程占有的资源

    4.循环等待:存在一个等待链,即T1等待T2占有的资源,T2等待T3占有的资源,T3等待T1占有的资源。

    如果要解决死锁,则需要破坏任意一死锁的必要条件。

    一.破坏占有且等待条件

     解决方法:只要限定所有资源锁同时获取,同时释放。就可以预防掉死锁。其实就是破坏掉占有且等待条件。

    下面以银行转账的代码为例子

    /**
     * 锁分配类(单例)
     *
     * @author Liumz
     * @since 2019-04-02  15:57:32
     */
    @Component
    public class Allocator {
        /**
         * 已被申请锁的集合
         */
        private List<Object> locks = new ArrayList<>();
        /**
         * 申请锁
         *
         * @param timeOut   过期时间(秒)
         * @param lockArray 要申请的锁集合
         */
        public synchronized void apply(int timeOut, Object... lockArray) throws Exception {
            //如果当前不满足申请条件,则等待。直到资源被释放时进行notifyAll唤醒当前线程
            //while(condition){wait()} 是一个标准范式,线程如果被唤醒,执行时会再判断一次条件。
            LocalDateTime dtStart = LocalDateTime.now();
            while (Arrays.stream(lockArray).anyMatch(i -> this.locks.contains(i))) {
                //时间间隔达到5秒还未获取到条件锁,则抛出异常
                if (Duration.between(dtStart, LocalDateTime.now()).toMillis() > timeOut * 1000) {
                    throw new Exception("放弃任务");
                }
                //释放当前对象锁,并等待
                try {
                    this.wait(1000);
                } catch (InterruptedException ignore) {
                }
            }
            //如果已被申请锁的集合中没有要申请的锁,表示申请成功,并把申请成功的锁加入集合
            this.locks.addAll(Arrays.asList(lockArray));
        }
        /**
         * 释放锁
         *
         * @param lockArray 要释放的锁集合
         */
        public synchronized void free(Object... lockArray) {
            for (Object o : lockArray) {
                this.locks.remove(o);
            }
            //唤醒所有wait的线程,正在等待locks被移除释放的线程。尽量使用notifyAll,避免有的线程会不被唤醒,一直wait
            this.notifyAll();
        }
    }
    View Code
    /**
     * 银行账户类
     *
     * @author Liumz
     * @since 2019-04-02  15:36:15
     */
    @Component
    @Scope("prototype")
    public class BankAccount {
        /**
         * 余额
         */
        private int balance;
        /**
         * 锁分配对象
         */
        @Autowired
        private Allocator allocator;
        /**
         * 转账
         *
         * @param target 目标账户
         * @param amount 转账金额
         */
        public void transfer(BankAccount target, int amount) {
            //申请锁,如果申请不到会一直等待。除非超时时抛出异常
            try {
                this.allocator.apply(5, target, this);
            } catch (Exception e) {
                return;
            }
            try {
                //同时锁定目标和当前账户,避免出现死锁情况.并且进行账户余额加减操作
                synchronized (this) {
                    synchronized (target) {
                        this.balance -= amount;
                        target.balance += amount;
                    }
                }
            } finally {
                //同时释放加的两个锁
                this.allocator.free(target, this);
            }
        }
    }
    View Code

    .破坏循环等待条件

     解决方法:对锁进行排序,每次申请锁需要按从小到大顺序申请。这样就不存在循环等待了

    /**
     * 银行账户类
     *
     * @author Liumz
     * @since 2019-04-02  15:36:15
     */
    @Component
    @Scope("prototype")
    public class BankAccount {
        /**
         * 余额
         */
        private int balance;
        /**
         * 序号id
         */
        private int id;
    
        /**
         * 转账
         *
         * @param target 目标账户
         * @param amount 转账金额
         */
        public void transfer(BankAccount target, int amount) {
            //对账户序号排序
            BankAccount firstLock = target;
            BankAccount secondLock = this;
            if (firstLock.id > secondLock.id) {
                firstLock = this;
                secondLock = target;
            }
            //先锁定序号小的账户,再锁定序号大的账户
            synchronized (firstLock){
                synchronized (secondLock){
                    this.balance -= amount;
                    target.balance += amount;
                }
            }
        }
    }
    View Code

    .破坏不可抢占条件

     解决方法: 使用 Lock 和UnLock,在finally里执行unlock,主动释放资源。此时别人就可以抢占了。

     

    活锁:

    多个线程获取不到资源,就放开已获得的资源,重试。相当于系统空转,一直在做无用功。

    例如,行人走路相向而行,互相谦让,一直重复谦让的过程。

    如以下一直死循环:

    start:
    p1 lock A
    p2 lock B
    p1 lock B failed
    p2 lock A failed 
    p1 release A
    p2 release B
    goto start

    解决方法:引入一些随机性,比如暂停随机时间重试。

    饥饿:

    1:优先级高的线程总是抢占到资源,而优先级低的线程可能会一直等待,从而无法获取资源无法执行;

    2:一个线程一直不释放资源,别的线程也会出现饥饿的情况。

    3:wait()等待情况下的线程一直都不被notify,而其他的线程总是能被唤醒

    解决方法:引入公平锁

    无锁:

    CAS(campare and swap):内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。CAS是原子操作,只有一条cpu指令

    无锁即不对资源锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突(CAS判断)就修改成功并退出否则就会继续下一次循环尝试。

    如jdk的基于CAS实现的原子操作类,就是对无锁的实现。 还有无锁队列,也是循环线程对变量进行CAS操作的数据结构。

    CAS的缺点:

    1.ABA问题:V值为A,T1,T2从内存取出V值为A.。然后T2 CAS修改变量V为B , 接着T2 又CAS修改变量V为A。这时T1 CAS 变量V时发现内存中V还是A ,CAS操作成功。

    2.循环消耗大

    3.只能保证一个共享变量的原子操作

  • 相关阅读:
    Linux04 目录的相关操作(mkdir、rmdir、rm、cp)
    【PDARTS】2019-ICCV-Progressive Differentiable Architecture Search Bridging the Depth Gap Between Search and Evaluation-论文阅读
    【NAS with RL】2017-ICLR-Neural Architecture Search with Reinforcement Learning-论文阅读
    智能手机娱乐时代的反思——《娱乐至死》读书笔记
    人必活着,爱才能有所附丽——《玩偶之家》读书笔记
    Brute Force暴力破解
    学生信息管理系统.cpp(大二上)
    通讯录管理系统(大一上)
    如何在Unity中画抛物线
    Unity2.5D Sprite层级显示遮挡问题处理
  • 原文地址:https://www.cnblogs.com/liumz0323/p/10633929.html
Copyright © 2020-2023  润新知