线程死锁

来源:互联网 发布:java数据结构与算法 编辑:程序博客网 时间:2024/06/02 19:28

线程死锁

安全性和活跃度通常是相互制约的,虽然可以通过开辟一定量的线程来提高活跃度,但是用来保证多线程安全的锁也可能引起锁顺序死锁问题(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();        }    }}

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

原创粉丝点击