死锁的原因及解决方法

❣️关注专栏: JavaEE


☘️1.什么是死锁

死锁是一个非常让程序猿烦恼的问题,一旦所写的程序有了死锁,那么程序就无法执行下去,会出现严重的 bug,并且死锁非常隐蔽,我们不会轻易发现它,在开发阶段,不经意期间我们就会写出死锁,很难检测出来。
那么什么是死锁呢?竟然让我们如此烦恼。
“死锁”就是2个或2个以上的线程互相持有对方想要的资源,导致各自处于阻塞等待状态,致使程序无法执行下去,这就是“死锁”。

☘️2.死锁的三个典型情况

☘️2.1情况一

一个线程一把锁,连续加两次。如果锁是不可重入锁,就会死锁。
Java 里的 synchronized 和 ReentrantLock 都是可重入锁

☘️2.2情况二

两个线程两把锁,t1 和 t2 各自对 锁A 和 锁B 加锁,再尝试获取对方的锁。线程在竞争资源,导致死锁。
下面图解展示以下这种情况:
在这里插入图片描述

☘️2.2.1死锁的代码展示

/**
     * 死锁
     * 两个线程两把锁,一个线程各对应一把锁,再获取另一个锁
     */
    public static void main(String[] args) {
        Object jiangyou = new Object();
        Object cu = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (jiangyou) {
                // 加入 sleep 都是为了让线程先把对应的第一个锁拿到
                // 加上休眠,表示让 jiangyou 的锁获取到之后,再获取 cu 的锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (cu) {
                    System.out.println("t1把酱油和醋都拿到了");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (cu) {
                // 加上休眠,表示让 cu 的锁获取到之后,再获取 jiangyou 的锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (jiangyou) {
                    System.out.println("t2把醋和酱油都拿到了");
                }
            }
        });

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

运行之后发现无法打印出结果,那么我们查看一下线程调用栈,按照这个路径查找到线程调用栈,我这里的jdk版本如下是jdk1.8.0_192,不同的版本名称不一样,你是 jdk1.9.0就点开 jdk1.9.0就行了。
在这里插入图片描述
点击jconsole.exe,按照以下步骤进行连接:有人可能不会出现第一步(第一步的这个名称是我 idea 中这个代码的类名),因为你可能没有运行该代码,你要保持这个代码一直在运行,不要停止运行,就会出现这个连接。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
同样点开 Thread-1 也会有这样的描述。
针对这样的死锁问题,需要借用这个 jconsole.exe 工具来帮助我们定位到死锁的地方。

☘️2.3多个线程多把锁

我们在学校学习的时候,最经典的就是哲学家就餐问题。一共有5个哲学家(A、B、C、D、E),一共有5根筷子(1、2、3、4、5),共同吃一碗面条。
在这里插入图片描述
每个哲学家有两种状态:
(1)思考问题(相当于线程中的阻塞状态)
(2)拿起筷子吃面条(相当于线程获取到锁之后执行一些计算)

由于操作系统是随时调度的,所以这五个哲学家,随时都可能想要吃面条,也随时都可能思考问题。正常来说,如果想要吃面条,需要拿起左手和右手的两根筷子。
假设此时出现了特殊情况:同一时刻,所有的哲学家都拿起了左手的筷子,需要吃面还需要拿起右手的筷子,就需要等待右边的哲学家把筷子放下,此时就是出现了死锁。

☘️3死锁产生的必要条件

☘️3.1互斥性

线程1 拿到了锁,线程2 就必须等着,所以线程之间是互斥使用锁的。

☘️3.2不可抢占

线程1 拿到了锁,必须是 线程1 主动释放,线程2 才能得到锁,如果 线程1 不主动释放锁,线程2 是不能强行获取到锁的。

☘️3.3请求和保持

线程1 拿到 锁A 之后,再尝试获取 锁B,A 这把锁还是保持的,不会因为 线程1 想要获取 锁B 就把 锁A 释放了。

☘️3.4循环等待

线程1 拿到 锁A 之后,尝试获取 锁B,线程2 拿到 锁B 之后,尝试获取锁A。
线程1 在获取 锁B 的时候等待 线程2 释放 B,同时线程2 在获取 A 的时候需要等待线程1 释放 A。

这4个必要条件缺一不可。前3点都是锁的基本特性,一般不需要我们程序猿自己去设置,只有第4个是唯一一个和代码相关的,也是程序猿可以控制的。

☘️4如何避免死锁

想要避免死锁,我们就必须打破必要条件,这里的突破口就是第四点的循环等待这一条件,我们可以给锁进行编号,然后制定一个固定的顺序(比如从小到大)来加锁。任意个线程加多把锁的时候都让线程按照上边的规则,就是按照固定的顺序加锁,自然循环等待的条件就会被打破,此时就会避免死锁了。

☘️4.1避免死锁代码

/**
     * 解决死锁:指定一个固定的顺序
     * 比如:都先拿 jiangyou 再拿 cu
     */
    public static void main1(String[] args) {
        Object jiangyou = new Object();
        Object cu = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (jiangyou) {
                // 加入 sleep 都是为了让线程先把对应的第一个锁拿到
                // 加上休眠,表示让 jiangyou 的锁获取到之后,再获取 cu 的锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (cu) {
                    System.out.println("t1把酱油和醋都拿到了");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (jiangyou) {
                // 加上休眠,表示让 cu 的锁获取到之后,再获取 jiangyou 的锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (cu) {
                    System.out.println("t2把醋和酱油都拿到了");
                }
            }
        });

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

因为指定了一个顺序:都先拿 jiangyou(酱油),再拿 cu(醋)。让线程 t1 把酱油和醋拿到之后就会释放锁,然后线程 t2 就会去获取,从而避免了循环等待对方了。运行结果如下:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值