线程死锁

线程死锁

安全性和活跃度通常是相互制约的,虽然可以通过开辟一定量的线程来提高活跃度,但是用来保证多线程安全的锁也可能引起锁顺序死锁问题(lock-ordering deadlock)。类似,我们使用线程池和信号量来约束资源的使用,但是也可能存在资源死锁(resource deadlock)。Java程序不能从死锁中恢复,所以能够避免死锁对于程序的设计十分重要。

死锁

哲学家就餐是多线程中很经典的问题,它就存在死锁风险。五位哲学家都抓住右边的叉子,同时等待左边的叉子而不放弃右边的叉子时,那么任意一位哲学家都吃不到面,从而饿死。在Executor任务执行中提交了子任务,同时当前线程池没有多余的线程可供使用,而母线程等待子线程的结果,子线程等待母线程的计算资源时,也会出现死锁问题。此外,当线程A占有对象锁L时,想要获得另一个对象锁M,线程B持有对象锁M,想要获得对象锁L,两个线程将永远等待下去,这种彼此等待的死锁称为“依赖死锁”。

当一个线程永远占有另一个线程所等待的锁,而另一个线程尝试去获得这个锁,那么它们将永远被阻塞。

  • 锁顺序死锁
    哲学家就餐问题中,每位哲学家在尝试获取叉子时,如果顺序不一致(不是按照一个方向,而是每位哲学家任意挑选左右两边的一只筷子),那么就可能导致锁顺序死锁问题,如下图所示。
    哲学家就餐问题线程竞争资源时死锁分析图

验证锁顺序的一致性需要对程序中锁的行为进行分析,单独观察每一个锁的代码路径并不能得出锁顺序死锁的结论。

public class Test {

            public static void main(String[] args) {
                Left left = new Left();
                Right right = new Right();

                left.setRight(right);
                right.setLeft(left);

                left.start();
                right.start();

            }

            public static class Left extends Thread{
                private Right right;

                public void setRight(Right right) {
                    this.right = right;
                }

                public void synchronizedLeft() {
                    synchronized(this) {
                        try {
                            Thread.sleep(10);
                            //如果把try...catch放到synchronized里面呢?
                            synchronized(right) {
                                right.sayMyName();  
                            }
                        } catch(Exception e) {
                                //操作
                        }
                    }
                }

                public void sayMyName() {
                    System.out.println("my name is Left");
                }

                @Override
                public void run() {
                    this.synchronizedLeft();
                }

            }

            public static class Right extends Thread{
                private Left left;

                public void setLeft(Left left) {
                    this.left = left;
                }

                public void synchronizedRight() {
                    synchronized(this) {
                        try {
                            Thread.sleep(10);
                            //如果把try...catch放到synchronized里面呢?
                            synchronized(left) {
                                left.sayMyName();   
                            }
                        } catch(Exception e) {
                                //操作
                        }
                    }
                }

                public void sayMyName() {
                    System.out.println("my name is Riht");
                }

                @Override
                public void run() {
                    this.synchronizedRight();
                }
            }   
        }

从上面的图片和代码中可以看出,锁顺序死锁造成原因是,线程A先锁定Left,在尝试获取Right锁时,Right锁已经被B线程锁定了,而B线程又在等待A线程释放Left锁。所以,如果线程A先锁定了Left,又在B之前锁定了Right,那么锁顺序死锁就不会发生。问题关键,是如何保证获取锁时,另一个线程不会占用资源。
银行进行转账时,两个账户间会不会有死锁问题存在?这也是死锁比较经典的场景。这种常见也比较明显,就是在同一个方法中获取两个锁。

  • 协作对象间的死锁
锁顺序死锁在代码分析上比较容易察觉,因为锁的获取口径比较直观,即synchronized嵌套了另一个synchronized。协作对象间的死锁,则不易察觉,因为对象间的方法调用带来了synchronized的嵌套。锁顺序是一种显示的锁定,而协作对象间的锁是隐士的锁顺序。
public class Test {
    public static void main(String[] args) {
        final Left left = new Left();
        final Right right = new Right();

        left.setRight(right);
        right.setLeft(left);

        left.start();
        right.start();
    }

    public static class Left extends Thread{
        private Right right;

        public void setRight(final Right right) {
            this.right = right;
        }

        /**
         * 从方法调用上,很难一眼看出synchronized嵌套
         */
        public synchronized void sayMyName1() {
            try {
                Thread.sleep(10);
                /*
                 * 这里是对象间协作时发生了死锁,因为Left获取了锁后,在right中有尝试获取锁,
                 * 也就是隐士synchronized嵌套,这种称为对象协作间死锁
                 */
                this.right.sayMyName1();
            } catch(Exception e) {
                //操作
            }
        }

        public synchronized void sayMyName2() {
            System.out.println("you name is Right");
        }

        public void run() {
            this.sayMyName1();
        }

    }

    public static class Right extends Thread{
        private Left left;

        public void setLeft(final Left left) {
            this.left = left;
        }

        public synchronized void sayMyName1() {
            System.out.println("you name is Left");
        }

        /**
         * 从方法调用上,很难一眼看出synchronized嵌套
         */
        public synchronized void sayMyName2() {
            try {
                Thread.sleep(10);
                /*
                 * 这里是对象间协作时发生了死锁,因为Right获取了锁后,在left中有尝试获取锁,
                 * 也就是隐士synchronized嵌套,这种称为对象协作间死锁
                 */
                this.left.sayMyName2();
            } catch(Exception e) {
                //操作
            }
        }

        public void run() {
            this.sayMyName2();
        }
    }
}

在持有锁的时候调用外部方法要小心死锁问题,因为外部方法可能会获取其他的锁,或者发生阻塞,当一个线程持有锁的时候会阻塞其他尝试获取该锁的线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值