• 死锁的3种死法


    1. 什么是死锁

    在多线程环境中,多个进程可以竞争有限数量的资源。当一个进程申请资源时,如果这时没有可用资源,那么这个进程进入等待状态。有时,如果所申请的资源被其他等待进程占有,那么该等待进程有可能再也无法改变状态。这种情况称为死锁

    在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直住,不再往下执行。我们只能通过中止并重启的方式来让程序重新执行。

    2. 造成死锁的原因

    • 当前线程拥有其他线程需要的资源
    • 当前线程等待其他线程已拥有的资源
    • 都不放弃自己拥有的资源

    3. 死锁的必要条件

    3.1 互斥

    进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

    3.2 不可剥夺

    进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

    3.3 请求与保持

    进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

    3.4 循环等待

    是指进程发生死锁后,必然存在一个进程–资源之间的环形链,通俗讲就是你等我的资源,我等你的资源,大家一直等。

    4. 死锁的分类

    4.1 静态顺序型死锁

    线程之间形成相互等待资源的环时,就会形成顺序死锁lock-ordering deadlock,多个线程试图以不同的顺序来获取相同的锁时,容易形成顺序死锁,如果所有线程以固定的顺序来获取锁,就不会出现顺序死锁问题

    经典案例是LeftRightDeadlock,两个方法,分别是leftRigth、rightLeft。如果一个线程调用leftRight,另一个线程调用rightLeft,且两个线程是交替执行的,就会发生死锁。

    public class LeftRightDeadLock {
    
        //左边锁
        private static Object left = new Object();
        //右边锁
        private static Object right = new Object();
    
        /**
         * 现持有左边的锁,然后获取右边的锁
         */
        public static void leftRigth() {
            synchronized (left) {
                System.out.println("leftRigth: left lock,threadId:" + Thread.currentThread().getId());
                //休眠增加死锁产生的概率
                sleep(100);
                synchronized (right) {
                    System.out.println("leftRigth: right lock,threadId:" + Thread.currentThread().getId());
                }
            }
        }
    
        /**
         * 现持有右边的锁,然后获取左边的锁
         */
        public static void rightLeft() {
            synchronized (right) {
                System.out.println("rightLeft: right lock,threadId:" + Thread.currentThread().getId());
                //休眠增加死锁产生的概率
                sleep(100);
                synchronized (left) {
                    System.out.println("rightLeft: left lock,threadId:" + Thread.currentThread().getId());
                }
            }
        }
    
        /**
         * 休眠
         *
         * @param time
         */
        private static void sleep(long time) {
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            //创建一个线程池
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            executorService.execute(() -> leftRigth());
            executorService.execute(() -> rightLeft());
            executorService.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    输出:

    leftRigth: left lock,threadId:12
    rightLeft: right lock,threadId:13
    
    • 1
    • 2

    我们发现,12号线程锁住了左边要向右边获取锁,13号锁住了右边,要向左边获取锁,因为两边都不释放自己的锁,互不相让,就产生了死锁。

    4.1.1 解决方案

    固定加锁的顺序(针对锁顺序死锁)

    只要交换下锁的顺序,让线程来了之后先获取同一把锁,获取不到就等待,等待上一个线程释放锁再获取锁。

    public static void leftRigth() {
           synchronized (left) {
             ...
               synchronized (right) {
                ...
               }
           }
       }
    
       public static void rightLeft() {
           synchronized (left) {
             ...
               synchronized (right) {
                ...
               }
           }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    4.2 动态锁顺序型死锁

    由于方法入参由外部传递而来,方法内部虽然对两个参数按照固定顺序进行加锁,但是由于外部传递时顺序的不可控,而产生锁顺序造成的死锁,即动态锁顺序死锁。

    上例告诉我们,交替的获取锁会导致死锁,且锁是固定的。有时候锁的执行顺序并不那么清晰,参数导致不同的执行顺序。经典案例是银行账户转账,from账户向to账户转账,在转账之前先获取两个账户的锁,然后开始转账,如果这是to账户向from账户转账,角色互换,也会导致锁顺序死锁。

    /**
     * 动态顺序型死锁
     * 转账业务
     */
    public class TransferMoneyDeadlock {
    
        public static void transfer(Account from, Account to, int amount) {
            //先锁住转账的账户
            synchronized (from) {
                System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁成功");
                //休眠增加死锁产生的概率
                sleep(100);
                //在锁住目标账户
                synchronized (to) {
                    System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁成功");
                    if (from.balance < amount) {
                        System.out.println("余额不足");
                        return;
                    } else {
                        from.debit(amount);
                        to.credit(amount);
                        System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱成功");
                    }
                }
            }
        }
    
        private static class Account {
            String name;
            int balance;
    
            public Account(String name, int balance) {
                this.name = name;
                this.balance = balance;
            }
    
            void debit(int amount) {
                this.balance = balance - amount;
            }
    
            void credit(int amount) {
                this.balance = balance + amount;
            }
        }
    
    
        /**
         * 休眠
         *
         * @param time
         */
        private static void sleep(long time) {
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            //创建线程池
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            //创建账户A
            Account A = new Account("A", 100);
            //创建账户B
            Account B = new Account("B", 200);
            //A -> B 的转账
            executorService.execute(() -> transfer(A, B, 5));
            //B -> A 的转账
            executorService.execute(() -> transfer(B, A, 10));
            executorService.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    输出:

    线程【12】获取【A】账户锁成功
    线程【13】获取【B】账户锁成功
    
    • 1
    • 2

    然后就没有然后了,产生了死锁,我们发现 因为对象的调用关系,产生了互相锁住资源的问题。

    4.2.1 解决方案

    根据传入对象的hashCode硬性确定加锁顺序,消除可变性,避免死锁。

    package com.test.thread.deadlock;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 动态顺序型死锁解决方案
     */
    public class TransferMoneyDeadlock {
        /**
         * 监视器,第三把锁,为了方式HASH冲突
         */
        private static Object lock = new Object();
    
        /**
         * 我们经过上一次得失败,明白了不能依赖参数名称简单的确定锁的顺序,因为参数是
         * 具有动态性的,所以,我们改变一下思路,直接根据传入对象的hashCode()大小来
         * 对锁定顺序进行排序(这里要明白的是如何排序不是关键,有序才是关键)。
         *
         * @param from
         * @param to
         * @param amount
         */
        public static void transfer(Account from, Account to, int amount) {
            /**
             * 这里需要说明一下为什么不使用HashCode()因为HashCode方法可以被重写,
             * 所以,我们无法简单的使用父类或者当前类提供的简单的hashCode()方法,
             * 所以,我们就使用系统提供的identityHashCode()方法,该方法保证无论
             * 你是否重写了hashCode方法,都会在虚拟机层面上调用一个名为JVM_IHashCode
             * 的方法来根据对象的存储地址来获取该对象的hashCode(),HashCode如果不重写
             * 的话,其实也是通过这个虚拟机层面上的方法,JVM_IHashCode()方法实现的
             * 这个方法是用C++实现的。
             */
            int fromHash = System.identityHashCode(from);
            int toHash = System.identityHashCode(to);
            if (fromHash > toHash) {
                //先锁住转账的账户
                synchronized (from) {
                    System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁成功");
                    //休眠增加死锁产生的概率
                    sleep(100);
                    //在锁住目标账户
                    synchronized (to) {
                        System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁成功");
                        if (from.balance < amount) {
                            System.out.println("余额不足");
                            return;
                        } else {
                            from.debit(amount);
                            to.credit(amount);
                            System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱成功");
                        }
                    }
                }
            } else if (fromHash < toHash) {
                //先锁住转账的账户
                synchronized (to) {
                    System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁成功");
                    //休眠增加死锁产生的概率
                    sleep(100);
                    //在锁住目标账户
                    synchronized (from) {
                        System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁成功");
                        if (from.balance < amount) {
                            System.out.println("余额不足");
                            return;
                        } else {
                            from.debit(amount);
                            to.credit(amount);
                            System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱成功");
                        }
                    }
                }
            } else {
                //如果传入对象的Hash值相同,那就加让加第三层锁
                synchronized (lock) {
                    //先锁住转账的账户
                    synchronized (from) {
                        System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁成功");
                        //休眠增加死锁产生的概率
                        sleep(100);
                        //在锁住目标账户
                        synchronized (to) {
                            System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁成功");
                            if (from.balance < amount) {
                                System.out.println("余额不足");
                                return;
                            } else {
                                from.debit(amount);
                                to.credit(amount);
                                System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱成功");
                            }
                        }
                    }
                }
            }
    
        }
    
        private static class Account {
            String name;
            int balance;
    
            public Account(String name, int balance) {
                this.name = name;
                this.balance = balance;
            }
    
            void debit(int amount) {
                this.balance = balance - amount;
            }
    
            void credit(int amount) {
                this.balance = balance + amount;
            }
        }
    
    
        /**
         * 休眠
         *
         * @param time
         */
        private static void sleep(long time) {
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            //创建线程池
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            //创建账户A
            Account A = new Account("A", 100);
            //创建账户B
            Account B = new Account("B", 200);
            //A -> B 的转账
            executorService.execute(() -> transfer(A, B, 5));
            //B -> A 的转账
            executorService.execute(() -> transfer(B, A, 10));
            executorService.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145

    输出

    线程【12】获取【A】账户锁成功
    线程【12】获取【B】账户锁成功
    线程【12】从【A】账户转账到【B】账户【5】元钱成功
    线程【13】获取【B】账户锁成功
    线程【13】获取【A】账户锁成功
    线程【13】从【B】账户转账到【A】账户【10】元钱成功
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4.3 协作对象间的死锁

    在协作对象之间可能存在多个锁获取的情况,但是这些获取多个锁的操作并不像在LeftRightDeadLock或transferMoney中那么明显,这两个锁并不一定必须在同一个方法中被获取。如果在持有锁时调用某个外部方法,那么这就需要警惕死锁问题,因为在这个外部方法中可能会获取其他锁,或者阻塞时间过长,导致其他线程无法及时获取当前被持有的锁。

    上述两例中,在同一个方法中获取两个锁。实际上,锁并不一定在同一方法中被获取。经典案例,如出租车调度系统。

    /**
     * 协作对象间的死锁
     */
    public class CoordinateDeadlock {
        /**
         * Taxi 类
         */
        static class Taxi {
            private String location;
            private String destination;
            private Dispatcher dispatcher;
    
            public Taxi(Dispatcher dispatcher, String destination) {
                this.dispatcher = dispatcher;
                this.destination = destination;
            }
    
            public synchronized String getLocation() {
                return this.location;
            }
    
            /**
             * 该方法先获取Taxi的this对象锁后,然后调用Dispatcher类的方法时,又需要获取
             * Dispatcher类的this方法。
             *
             * @param location
             */
            public synchronized void setLocation(String location) {
                this.location = location;
                System.out.println(Thread.currentThread().getName() + " taxi set location:" + location);
                if (this.location.equals(destination)) {
                    dispatcher.notifyAvailable(this);
                }
            }
        }
    
        /**
         * 调度类
         */
        static class Dispatcher {
            private Set<Taxi> taxis;
            private Set<Taxi> availableTaxis;
    
            public Dispatcher() {
                taxis = new HashSet<Taxi>();
                availableTaxis = new HashSet<Taxi>();
            }
    
            public synchronized void notifyAvailable(Taxi taxi) {
                System.out.println(Thread.currentThread().getName() + " notifyAvailable.");
                availableTaxis.add(taxi);
            }
    
            /**
             * 打印当前位置:有死锁风险
             * 持有当前锁的时候,同时调用Taxi的getLocation这个外部方法;而这个外部方法也是需要加锁的
             * reportLocation的锁的顺序与Taxi的setLocation锁的顺序完全相反
             */
            public synchronized void reportLocation() {
                System.out.println(Thread.currentThread().getName() + " report location.");
                for (Taxi t : taxis) {
                    t.getLocation();
                }
            }
    
            public void addTaxi(Taxi taxi) {
                taxis.add(taxi);
            }
        }
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            final Dispatcher dispatcher = new Dispatcher();
            final Taxi taxi = new Taxi(dispatcher, "软件园");
            dispatcher.addTaxi(taxi);
            //先获取dispatcher锁,然后是taxi的锁
            executorService.execute(() -> dispatcher.reportLocation());
            //先获取taxi锁,然后是dispatcher的锁
            executorService.execute(() -> taxi.setLocation("软件园"));
            executorService.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    4.3.1 解决方案

    使用开放调用,开放调用指调用该方法不需要持有锁。

    开放调用,是指在调用某个方法时不需要持有锁。开放调用可以避免死锁,这种代码更容易编写。上述调度算法完全可以修改为开发调用,修改同步代码块的范围,使其仅用于保护那些涉及共享状态的操作,避免在同步代码块中执行方法调用。修改Dispatcher的reportLocation方法:

    4.3.1.1 setLocation方法
    /**
        * 开放调用,不持有锁期间进行外部方法调用
        *
        * @param location
        */
       public void setLocation(String location) {
           synchronized (this) {
               this.location = location;
           }
           System.out.println(Thread.currentThread().getName() + " taxi set location:" + location);
           if (this.location.equals(destination)) {
               dispatcher.notifyAvailable(this);
           }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    4.3.1.2 reportLocation 方法
    /**
           * 同步块只包含对共享状态的操作代码
           */
          public synchronized void reportLocation() {
              System.out.println(Thread.currentThread().getName() + " report location.");
              Set<Taxi> taxisCopy;
              synchronized (this) {
                  taxisCopy = new HashSet<Taxi>(taxis);
              }
              for (Taxi t : taxisCopy) {
                  t.getLocation();
              }
          }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    本文由传智教育博学谷教研团队发布。

    如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

    转载请注明出处!

  • 相关阅读:
    两种高效的事件处理模式:Reactor模式与Proactor模式
    微服务项目雪崩的解决思路
    83、【backtrader期货策略】一个国债期货的日线趋势跟踪策略
    番外6:下载+安装+配置Linux
    8. JVM-堆
    红黑树的插入与验证——附图详解
    【Node.js】基于cors解决接口跨域的问题
    腾讯云产品可观测最佳实践 (Function)¶
    机器学习 - 混淆矩阵:技术与实战全方位解析
    vue3中的$refs 和$parent
  • 原文地址:https://blog.csdn.net/bxg_kyjgs/article/details/127750042