三分钟秒懂死锁产生原因!

开发 前端
我们知道被synchronized修饰的代码,当一个线程持有一个锁,其它线程尝试去获取这个锁未获取到时,那么其它线程会进入阻塞状态,直到线程释放锁才能再次拥有获取锁的条件。

一、简介

在之前的文章中,我们介绍了synchronized同步锁关键字的作用以及相关的用法,它能够保证同一时刻最多只有一个线程执行修饰的代码段,以实现线程安全执行的效果。

但是如果过度的使用synchronized等方式进行加锁,程序可能会出现死锁现象。

那什么是死锁呢?它有什么危害?

我们知道被synchronized修饰的代码,当一个线程持有一个锁,其它线程尝试去获取这个锁未获取到时,那么其它线程会进入阻塞状态,直到线程释放锁才能再次拥有获取锁的条件。假如线程 A 持有锁 L 并且想获取锁 R,线程 B 持有锁 R 并且想获取锁 L,那么这两个线程将会永久等待下去,这种情况就是最简单的死锁现象。

如果程序出现了死锁,会给系统功能带来非常严重的问题,轻则导致程序响应时间变长,系统吞吐量变小;重则导致应用中的某一个功能直接失去响应能力无法提供服务,因此我们应该及时发现并避规这些问题。

当然发生死锁的软件应用,不仅限于 Java 程序,还有数据库等,不同的是:数据库系统中设计了死锁的检测以及从死锁中恢复的机制,数据库如果检测到一组事务中发生了死锁,将选择一个牺牲者并放弃这个事务。

而 Java 虚拟机解决死锁问题并没有数据库那么强大,在 Java 程序中,采用synchronized加锁的代码如果发生死锁,两个线程就不能再使用了,并且这两个线程所在的同步代码/代码块也无法再运行了,除非杀掉服务,然后重启服务!

在实际的软件项目开发过程中,死锁其实是编程设计上的 bug,问题也比较隐晦,即使通过压力测试也不一定能找到程序上的死锁问题。死锁的出现,往往是在高负载的情况下产生,这种场景下比较难定位。

二、死锁复现

下面我们先来看一个比较经典的产生死锁示例代码。

public class DeadLock {

    private final Object right = new Object();

    private final Object left = new Object();

    /**
     * 加锁顺序从left -> right
     */
    public void leftRight() throws Exception {
        synchronized (left) {
            // 模拟某个业务操作耗时
            Thread.sleep(1000);
            synchronized (right) {
                System.out.println(Thread.currentThread().getName() + " left -> right lock.");
            }
        }
    }

    /**
     * 加锁顺序right -> left
     */
    public void rightLeft() throws Exception {
        synchronized (right) {
            // 模拟某个业务操作耗时
            Thread.sleep(1000);
            synchronized (left) {
                System.out.println(Thread.currentThread().getName() + " right -> left lock.");
            }
        }
    }
}
public class MyThreadA extends Thread {


    private DeadLock deadLock;

    public MyThreadA(DeadLock deadLock) {
        this.deadLock = deadLock;
    }

    @Override
    public void run() {
        try {
            deadLock.leftRight();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class MyThreadB extends Thread {


    private DeadLock deadLock;

    public MyThreadB(DeadLock deadLock) {
        this.deadLock = deadLock;
    }

    @Override
    public void run() {
        try {
            deadLock.rightLeft();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();

        MyThreadA threadA = new MyThreadA(deadLock);
        MyThreadB threadB = new MyThreadB(deadLock);
        threadA.start();
        threadB.start();
    }
}

运行测试类观察控制台,你会发现服务一直在运行,什么都没有输出,并且无法关闭,因为程序已经死锁了!

图片图片

发生这个现象的原因,其实也很简单。

1.线程 A 启动之后,先获取了left对象锁,然后紧接着尝试获取right对象锁,因为right对象锁被其它线程占有,只能进入阻塞状态

2.线程 B 启动之后,先获取了right对象锁,然后紧接着尝试获取left对象锁,因为left对象锁被其它线程占有,只能进入阻塞状态

3.两个线程互相等待对方释放锁,程序进入永久等待状态,因此都无法进入打印方法体

如何定位死锁问题呢?

我们可以通过 Java 自带的 jps 和 jstack 工具,查看 java 进程 id 和相关的线程堆栈信息。

定位过程如下!

2.1、通过 jps 获得当前 Java 虚拟机进程的 pid

图片图片

左边的是当前 Java 虚拟机进程 ID,后边是进程名称,其中MyThreadTest就是我们当前运行的测试类服务。

2.2、通过 jstack 查看进程中的线程信息

在 jstack 后面输入对应的 java 进程 ID,然后回车即可查询到进程中的线程情况,前面的部分,可以很清晰的看到,两个线程都处于阻塞状态,等待获取对应的锁。

图片图片

因为线程的信息比较多,直接滑倒最底部,可以看到 JVM 给出的死锁报告信息。

图片图片

遇到这种情况,只能强制终止服务才能解除死锁!

三、避免死锁的方式

上面我们复现了死锁的发生,总结下来你会发现死锁的产生,总共有四个共同特点:

1.互斥使用,即当资源被一个线程占用时,别的线程不能使用

2.不可抢占,资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放

3.请求和保持,当资源请求者在请求其他资源的同时保持对原有资源的占有

4.循环等待,多个线程存在环路的锁依赖关系而永远等待下去,例如 T1 占有 T2 的资源,T2 占有 T3 的资源,T3 占有 T1 的资源,这种情况可能会形成一个等待环路

这四个特点是死锁产生的必要条件,只要系统发生死锁,这些条件必然成立,只要能破坏其中一条即可让死锁消失,当然条件一是基础,不能被破坏。

理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免产生死锁和解除死锁。

在软件编程中,我们如何避免死锁呢?

关于死锁的避免,主要有以下几种方式:

1.尽可能使用无锁编程,使用开放调用的编码设计

2.设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量

2.尽可能的缩小锁的范围,防止锁住的资源过多引发阻塞

4.使用定时锁,比如Lock类中的tryLock方法去尝试获取锁,这个方法支持在指定时间内获取锁,如果等待超时会返回一个失败信息,死锁会自动解除。

对于死锁的诊断,主要有以下几种方式:

1.对代码进行全局分析,找出代码中什么地方会出现死锁

2.通过线程转储(Thread Dump)信息来分析死锁,比如 jstack、jvisualvm、jconsole 等工具

至于死锁的解除,主要有以下几种方式:

1.直接强制终止并重启服务,如果代码上的风险没有消除,可能还会再次出现

2.采用定时锁方案,虽然synchronized不具备这个功能,但是Lock类中的tryLock方法具备,实际编程中采用Lock中的超时机制进行加锁,应用的比较多

责任编辑:武晓燕 来源: Java极客技术
相关推荐

2024-06-06 08:50:43

2024-08-05 09:05:44

2024-05-16 11:13:16

Helm工具release

2009-11-09 12:55:43

WCF事务

2024-12-18 10:24:59

代理技术JDK动态代理

2023-12-27 08:15:47

Java虚拟线程

2024-08-30 08:50:00

2024-01-16 07:46:14

FutureTask接口用法

2022-02-17 09:24:11

TypeScript编程语言javaScrip

2021-04-20 13:59:37

云计算

2020-06-30 10:45:28

Web开发工具

2013-06-28 14:30:26

棱镜计划棱镜棱镜监控项目

2021-12-17 07:47:37

IT风险框架

2009-11-05 16:04:19

Oracle用户表

2024-07-05 09:31:37

2020-06-29 07:42:20

边缘计算云计算技术

2024-10-15 09:18:30

2024-01-12 07:38:38

AQS原理JUC

2021-02-03 14:31:53

人工智能人脸识别

2023-12-04 18:13:03

GPU编程
点赞
收藏

51CTO技术栈公众号