死锁问题及解决方案

前言

前面我们介绍了synchronized加锁能解决线程安全问题,但是不正确的加锁也会导致线程不安全,死锁就是不正确加锁导致的线程不安全问题.

1.死锁的三种经典场景

1.一个线程一把锁

如果锁是不可重入锁,并且一个线程对这把锁加锁了两次就会出现死锁问题.这就相当于把钥匙所在屋里了.

 第二次加锁需要第一次加的锁解锁,但是第二次加锁不成功正在阻塞

2.两个线程两把锁       

synchronized锁具有互斥性,即一个线程对一个对象加锁成功,另一个线程对该对象加锁就会发生阻塞.

设想一下这样一个场景,t1线程需要对A,B对象加锁,t2线程也需要对A,B对象加锁.如果t1先对A加锁在对B加锁,t2先对B加锁在对A加锁,但是执行过程中t1对A加锁成功,在对B加锁,但是B的锁已经被t2线程拿到了,t1就会发生阻塞,这时t2线程对A线程加锁也会失败,因为A对象已经被t1线程加锁成功了.这个问题就是死锁问题.

举个例子,这就相当于你家里有两套房子,一套房子A,一套房子B.房子A的钥匙被锁在了B房子中,B房子的钥匙被锁在了A房子中.这时你想要进入任何一个房子都打开另一个房子的大门,这种问题就是死锁问题.

下面看一个代码案例

package blogs;

public class Five {
    private static Object lockerA = new Object();
    private static Object lockerB = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (lockerA) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lockerB){
                    System.out.println("t1拿到了两把锁!");
                }
            }
        });

        Thread t2 = new Thread(()->{
            synchronized (lockerB) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lockerA) {
                    System.out.println("t2拿到了两把锁!");
                }
            }

        });

        t1.start();
        t2.start();
    }
}

从运行结果上我们可以看到进程一直在等待,这就是典型的死锁问题.

3.N个线程M把锁

这个问题有一个典型的例子就是哲学家就餐问题

有一个桌子,围着一群哲学家,哲学家两两之间放着一根筷子,而哲学家只会做两件事:吃面(获取到锁,执行后续代码),思考人生(阻塞等待).

 筷子就是锁对象,只有拿起两根筷子才能吃饭.当哲学家思考人生的时候,才会放下左右手的筷子.

如果发生极端情况,五个哲学家同时都拿起左手边的筷子,或者同时都拿起右手边的筷子,就会发现另一边的筷子被别人拿走了,都要等别的哲学家把另一边的筷子放下.由于每个哲学家都想别的哲学家放下筷子,此时就会形成死锁现象.

2.避免死锁问题

1.产生死锁死锁的四个必要条件

1.互斥使用:t1线程拿到锁,t2线程如果也想拿就必须阻塞等待,等t1线程释放.

2.不可抢占:一个线程拿到锁之后,只能主动解锁,别的线程不能强制抢占.

3.请求保持:一个线程拿到A锁之后,在获取B锁,拿到B锁之后不会释放A锁.

4.循环等待:当t1线程尝试获取锁A和B,t2线程尝试获取锁B和A.如果t1线程拿到了A锁,t2线程拿到了B锁,此时t1线程就会等待t2线程释放锁B,t2线程也会等待t1释放锁A.

这4个条件之中前三个都是都是synchronized锁的特性,都修改不了唯一能修改的只有第四条

2.打破循环等待

打破循环等待就需要对代码结构做出调整,就像前面举的例子,把t1线程和t2线程都改为先对A加锁在对B加锁,那么就能解决这个问题.

package blogs;

public class Five {
    private static Object lockerA = new Object();
    private static Object lockerB = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (lockerA) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lockerB){
                    System.out.println("t1拿到了两把锁!");
                }
            }
        });

        Thread t2 = new Thread(()->{
            synchronized (lockerA) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lockerB) {
                    System.out.println("t2拿到了两把锁!");
                }
            }

        });

        t1.start();
        t2.start();
    }
}

这样就能避免出现死锁问题. 

对于哲学家就餐问题也是一样的

我们做出规定,每个哲学家只能先拿自己左右两边编号小的筷子,在拿编号大的筷子

 这样有一个线程开始执行了之后,等它执行完就会把锁资源释放出来,那么就能被其他线程获取到,别的线程就不会一直死等,死锁问题也就自然被避免了.

3.银行家算法

这种方法解决死锁问题在实际开发中,并不推荐使用,比起上述方法更加复杂,也更容易出错,所以更推荐上述方法!

如果对银行家算法感兴趣的,可以自行查询相关资料.

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值