从零开始学Java之死锁的产生及解决办法有哪些?

作者 :孙玉昌,昵称【一一哥 】,另外【壹壹哥】也是我哦

千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者

前言

在前面的文章中,壹哥 多次给大家提到过"死锁"的概念,但有些小伙伴却对"死锁"不甚了了。所以今天壹哥再通过一篇文章,来给大家专门讲解死锁的内容,希望大家认真掌握哦。

------------------------------前戏已做完,精彩即开始----------------------------

全文大约【1800】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

配套开源项目资料

Github: github.com/SunLtd/Lear...

Gitee: gitee.com/sunyiyi/Lea...

一. 死锁

1. 概念

Java中的死锁是指多个线程同时占用一些共享资源且彼此相互等待,从而导致所有的线程都被阻塞,不能继续执行程序的情况。这就好比在一个十字路口,没有交警也没有红绿灯指挥通行,所有的车辆都占据道路且互相等待对方让出路权,此时就很容易造成道路堵死,这其实就是道路的"死锁"。如下图所示:

2. 死锁案例

虽然我们现在已经知道了死锁的概念,但具体什么时候会产生死锁,壹哥 相信很多小伙伴肯定还是弄不不清楚。所以接下来壹哥就给大家设计一个会产生死锁的代码案例,如下所示:

java 复制代码
/**
 * @author 一一哥Sun
 * @company 千锋教育
 */
public class Demo21 {
    // 定义2个锁定的对象
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void method1() {
        // 锁定对象1
        synchronized (lock1) {
            System.out.println("Method 1: 获取对象lock1的锁");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 锁定对象2
            synchronized (lock2) {
                System.out.println("Method 1: 获取对象lock2的锁");
            }
        }
     }

    public void method2() {
        // 锁定对象2
        synchronized (lock2) {
            System.out.println("Method 2: 获取对象lock2的锁");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 锁定对象1
            synchronized (lock1) {
                System.out.println("Method 2: 获取对象lock1的锁");
            }
        }
    }

    public static void main(String[] args) {
        final Demo21 example = new Demo21();

        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                example.method1();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                example.method2();
            }
        });

        //开启线程
        thread1.start();
        thread2.start();
    }
}

在上面的案例中,壹哥定义了两个方法method1和method2,这两个方法分别占用了lock1和lock2两个锁,并且在执行过程中会互相等待对方的锁,从而形成了死锁。如果我们运行该程序,就会看到两个线程互相等待对方释放自己占用的锁,这最终会导致所有的线程都被阻塞。上述案例中死锁的产生原因,如下图所示:

根据上面的案例,你可以总结出会导致死锁的条件吗?我们继续往下看。

3. 产生条件

其实一个Java程序要想产生死锁,也并不是那么容易,只有同时满足以下条件才行:

  1. 互斥条件:多个线程需同时访问一个共享资源,但每次只能有一个线程访问该资源;
  2. 请求和保持条件:一个线程在持有一个资源的同时,还想请求另一个资源;
  3. 不可剥夺条件:已经分配的资源不能被其他线程剥夺;
  4. 循环(环路)等待条件:多个线程形成了一个循环等待资源的链路,例如线程A等待线程B释放自己所占用的资源,线程B等待线程C释放自己所占用的资源,而线程C又等待线程A释放自己所占用的资源。

只有同时满足了以上条件,程序中才会产生死锁。既然我们现在知道了死锁的产生条件,那又该怎么解决呢?

4. 解决办法

我们知道,当出现死锁时,所有的线程都会被阻塞,且不能再继续执行程序,所以我们必须解决死锁。一般情况下,我们可以通过以下方式来避免线程死锁:

  1. 避免使用多个锁;
  2. 尽可能减少同步代码块的长度;
  3. 尝试改变锁的获取顺序,避免线程之间形成循环等待;
  4. 使用定时锁,当等待时间超过一定的时间值后就自动释放锁

以上就是打破死锁条件的解决办法,但是具体放到Java代码中又是怎么样的呢?接下来壹哥就把上面产生死锁的代码修改一下,解决死锁问题。

5. 案例优化

接下来壹哥就把上面产生死锁的案例优化一下,解决掉案例中的死锁,代码如下:

java 复制代码
/**
 * @author 一一哥Sun
 * @company 千锋教育
 */
public class Demo22 {
    // 定义2个锁定的对象
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void method1() {
        //锁定对象
        synchronized(lock1) {
            System.out.println("Method 1: 获取对象锁lock 1");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            synchronized(lock2) {
                System.out.println("Method 1: 获取对象锁lock 2");
            }
        }
    }

    public void method2() {
        synchronized(lock1) {
            System.out.println("Method 2: 获取对象锁lock 1");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            synchronized(lock2) {
                System.out.println("Method 2: 获取对象锁lock 2");
            }
        }
    }

    public static void main(String[] args) {
        final Demo22 demo = new Demo22();

        //定义两个线程
        Thread thread1 = new Thread(new Runnable() {
        	@Override
            public void run() {
            	demo.method1();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
        	@Override
            public void run() {
            	demo.method2();
            }
        });

        //开启线程
        thread1.start();
        thread2.start();
    }
}

上面的这个案例与之前的案例代码几乎一样,但与之不同的是,本案例中的方法method1和method2,都是先占用lock1锁,再占用lock2锁,这样就避免了死锁的发生,因为这两个方法占用锁的顺序是一致的。所以我们在编写多线程代码时,需要特别注意线程死锁的问题,避免影响程序的正常执行。

------------------------------正片已结束,来根事后烟----------------------------

二. 结语

至此,壹哥就把Java中的死锁给大家讲解完毕了,现在你明白了吗?我们在面试时经常会有面试官考察死锁相关的内容,比如死锁是怎么产生的?如何避免死锁?所以今天的内容很重要,请各位一定要牢牢掌握哦。

另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。

相关推荐
生擒小朵拉3 分钟前
STM32添加库函数
java·javascript·stm32
Z_z在努力9 分钟前
【杂类】Spring 自动装配原理
java·spring·mybatis
程序员爱钓鱼20 分钟前
Go语言实战案例-开发一个Markdown转HTML工具
前端·后端·go
小小菜鸡ing36 分钟前
pymysql
java·服务器·数据库
getapi39 分钟前
shareId 的产生与传递链路
java
桦说编程1 小时前
爆赞!完全认同!《软件设计的哲学》这本书深得我心
后端
thinktik1 小时前
还在手把手教AI写代码么? 让你的AWS Kiro AI IDE直接读飞书需求文档给你打工吧!
后端·serverless·aws
我没想到原来他们都是一堆坏人2 小时前
(未完待续...)如何编写一个用于构建python web项目镜像的dockerfile文件
java·前端·python
沙二原住民2 小时前
提升数据库性能的秘密武器:深入解析慢查询、连接池与Druid监控
java·数据库·oracle