对于我们开发者来说,出去面试的时候,经常会被问到一个问题,请谈谈你对死锁问题的理解?但是很多人都不能系统地回答出核心原理,有的人不知道如何排查死锁问题,有的人则不知道如何来解决死锁问题。那么到底该怎么系统地回答这个问题呢?今天我们就来聊聊这个话题。
在这节课,我会先给你介绍下死锁的概念,然后基于一个小场景,来模拟死锁问题的产生,并利用工具来排查死锁问题,最后我们再来看怎么在开发过程中避免死锁问题的产生。
# 死锁的概念
死锁一般发生在多线程执行的过程中,也就是两个或两个以上的线程在执行的时候,因为争夺资源会造成线程间互相等待,这种情况就是产生了死锁问题,在没有外力作用的情况下,这些线程会一直相互等待,没办法继续运行。
就比如这张图(图 1),你可以看到有两个资源,资源 1 和资源 2,和两个线程,分别为线程 A 和线程 B。线程 A 在已经获取了资源 2 的情况下,期望获取线程 B 持有的资源 1;而线程 B 在已经获取了资源 1 的情况下,期望获取线程 A 持有的资源 2,那么线程 A 和线程 B 就处于了相互等待的死锁状态。在没有外力干涉的情况下,线程 A 和线程 B 就会一直处于相互等待状态,不能处理其他任务,那这两个线程也就白白浪费掉了。
死锁产生的四个必要条件
对应死锁的概念,我们来看线程死锁问题产生的条件。相信你在学习操作系统时,就知道线程死锁需要四个必要条件:
第一,互斥条件。指的是多个线程不能同时使用同一个资源,比如线程 A 已经持有的资源,不能同时在被线程 B 持有。如果线程 B 请求获取被线程 A 已经占有的资源,那线程 B 只能等,等到这个资源被线程 A 释放。
第二,持有并等待条件。指的是当线程 A 已经持有了资源 1,又提出想申请资源 2,但是资源 2 已经被线程 C 占有了,所以线程 A 就会处于等待状态,但它在等待资源 2 的同时并不会释放自己已经获取的资源 1。
第三,不可剥夺条件。是指线程 A 获取到资源 1 后,在自己使用完之前不能被其它线程比如线程 B 抢占使用。如果线程 B 也想使用资源 1,只能在线程 A 使用完主动释放后获取。
第四,环路等待条件。在发生死锁的时候,必然存在一个线程,也就是资源的环形链,比如线程 A 已经获取了资源 2,但是请求获取资源 1;线程 B 已经获取了资源 1,但是请求获取资源 2,这就会形成一个线程和资源请求等待的环形图。
死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生。那么,我们该如何尽早排查死锁问题呢?
# 模拟死锁问题与排查
下面我们使用 Java 代码来模拟一个死锁场景:
~~~
public class DeadLockDemo {
...
// 1. 创建资源
private static Object resourceA = new Object();
private static Object resourceB = new Object();
public static void main(String[] args) {
//2. 创建线程 A
Thread threadA = createThreadA();
//3. 创建线程 B
Thread threadB = createThreadB();
//4. 启动线程
threadA.start();
threadB.start();
}
}
~~~
~~~
private static Thread createThreadA() {
Thread threadA = new Thread(() -> {
//2.1 尝试获取资源 A
synchronized (resourceA) {
System.out.println(Thread.currentThread() + " got ResourceA");
//2.2 休眠 1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get ResourceB");
//2.3 尝试获取资源 B
synchronized (resourceB) {
System.out.println(Thread.currentThread() + "got ResourceB");
}
}
}, "ThreadA");
return threadA;
}
~~~
~~~
private static Thread createThreadB() {
Thread threadB = new Thread(() -> {
//3.1 尝试获取资源 B
synchronized (resourceB) {
System.out.println(Thread.currentThread() + " got ResourceB");
//3.2 休眠 1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get ResourceA");
//3.3 尝试获取资源 A
synchronized (resourceA) {
System.out.println(Thread.currentThread() + "got ResourceA");
}
}
}, "ThreadB");
return threadB;
}
~~~
你可以看到,代码 1 创建了两个资源对象,分别为 resourceA 和 resourceB;
代码 2createThreadA 方法创建了一个名称为 ThreadA 的线程,这个线程启动后会先执行代码 2.1 的 synchronized 块试图获取 resourceA 上的对象锁,它获取成功后会休眠 1 秒,然后执行代码 2.3 尝试使用 synchronized 块获取 resourceB 上的对象锁。
代码 3createThreadB 是创建了一个名称为 ThreadB 的线程,这个线程启动后会先执行代码 3.1 的 synchronized 块试图获取 resourceB 上的对象锁,它获取成功后也会休眠 1 秒,然后执行代码 2.3 尝试使用 synchronized 块获取 resourceA 上的对象锁。
代码 4 则启动两个线程运行;运行上面代码后,可能会输出这样的结果:
~~~
Thread[ThreadA,5,main] got ResourceA
Thread[ThreadB,5,main] got ResourceB
Thread[ThreadA,5,main]waiting get ResourceB
Thread[ThreadB,5,main]waiting get ResourceA
~~~
从这段输出中,我们可以发现 ThreadA 一直卡到获取 ResourceB 的地方,ThreadB 则一直卡在获取 ResourceA 的地方,从而导致程序无法正常向下运行。那么为啥会卡到这里呢?
下面我们使用 JDK 自带的打印线程堆栈的 jstack pid(进程 ID) 命令,看下当前 JVM 中的线程堆栈,对应上面输出结果的一个线程堆栈是这样的:
~~~
Found one Java-level deadlock:
=============================
"ThreadB":
waiting to lock monitor 0x00007f886e832168 (object 0x000000076b839ff0, a java.lang.Object),
which is held by "ThreadA"
"ThreadA":
waiting to lock monitor 0x00007f886e8349f8 (object 0x000000076b83a000, a java.lang.Object),
which is held by "ThreadB"
Java stack information for the threads listed above:
===================================================
"ThreadB":
at org.mysql.DeadLockDemo.lambda$main$1(DeadLockDemo.java:49)
- waiting to lock <0x000000076b839ff0> (a java.lang.Object)
- locked <0x000000076b83a000> (a java.lang.Object)
at org.mysql.DeadLockDemo$$Lambda$2/577405636.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"ThreadA":
at org.mysql.DeadLockDemo.lambda$main$0(DeadLockDemo.java:31)
- waiting to lock <0x000000076b83a000> (a java.lang.Object)
- locked <0x000000076b839ff0> (a java.lang.Object)
at org.mysql.DeadLockDemo$$Lambda$1/2011482127.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
~~~
从这个线程堆栈我们可以知道什么呢?有一处死锁。其中 ThreadB 获取到了地址为 0x000000076b83a000 的对象(resourceB)的锁,然后等待获取地址为 0x000000076b839ff0 的对象(resourceA)的锁;而 ThreadA 获取到了地址 0x000000076b839ff0 对象(resourceA)的锁,然后等待获取地址为 0x000000076b83a000 的对象(resourceB)的锁。这解释了 ThreadA 为啥一直卡到获取 ResourceB 的地方,而 ThreadB 一直卡在获取 ResourceA 的地方。
经过我们刚才的分析,就能知道代码是出现了死锁问题,导致线程被阻塞,从而导致被阻塞的线程不能继续向下运行了。那我们该怎么修改前面那段代码,从而避免死锁呢?
# 如何避免死锁问题的产生
刚才我们也说了,死锁的产生需要同时满足四个必要条件,反过来说,预防死锁就只需要我们至少破坏其中一个条件。最常见的并且可行的就是使用资源有序分配法来破坏循环等待条件,从而避免死锁的产生。那什么是资源有序分配呢?
比如前面的代码例子,ThreadA 是先尝试获取 ResourceA,然后尝试获取资源 ResourceB;而 ThreadB 则是先尝试获取资源 ResourceB,然后尝试获取资源 ResourceA;这就不是资源有序分配的,因为 ThreadA 和 ThreadB 获取资源的顺序不一样。
资源有序分配是指当 ThreadA 是先尝试获取 ResourceA,然后尝试获取资源 ResourceB 时,ThreadB 也是先尝试获取 ResourceA,然后尝试获取资源 ResourceB;或者当 ThreadA 是先尝试获取 ResourceB,然后尝试获取资源 ResourceA,ThreadB 也是先尝试获取 ResourceB,然后尝试获取资源 ResourceA。也就是 ThreadA 和 ThreadB 总是以相同的顺序申请自己想要的资源。
我们可以使用资源有序分配法修改上面的例子,其中我们保持 createThreadA 方法,不变,createThreadB 代码修改为:
~~~
private static Thread createThreadB() {
Thread threadB = new Thread(() -> {
//3.1 尝试获取资源 A
synchronized (resourceA) {
System.out.println(Thread.currentThread() + " got ResourceA");
//3.2 休眠 1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get ResourceB");
//3.3 尝试获取资源 B
synchronized (resourceB) {
System.out.println(Thread.currentThread() + "got ResourceB");
}
}
}, "ThreadB");
return threadB;
}
~~~
你可以看到,代码 ThreadA 是先尝试获取 ResourceA,然后尝试获取资源 ResourceB;ThreadB 则也是先尝试获取 ResourceA,然后尝试获取资源 ResourceB,符合资源有序分配的原则。
然后运行刚才那段代码,一个可能的输出结果是这样的:
~~~
Thread[ThreadA,5,main] got ResourceA
Thread[ThreadA,5,main]waiting get ResourceB
Thread[ThreadA,5,main]got ResourceB
Thread[ThreadB,5,main] got ResourceA
Thread[ThreadB,5,main]waiting get ResourceB
Thread[ThreadB,5,main]got ResourceB
~~~
我们可以看到 ThreadA 先后获取到资源 ResourceA 和 ResourceB,然后线程 B 也先后获取到资源 ResourceA 和 ResourceB,最后程序正常终止运行,就不会出现死锁现象。
好了,关于线程死锁问题的产生与避免,我们今天就讲到这里。简单来说,死锁问题的产生是由两个或两个以上线程并行执行的时候,争夺资源而互相等待造成的。死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生,所以要避免死锁问题,最简单的办法就是用资源有序分配法来破坏循环等待条件。
在日常开发环境中,死锁问题的发生还是比较常见的,比如批量更新数据库时,如果不在插入前使用资源有序分配法对批量数据根据唯一键排序,也会发生死锁现象的。所以,建议你在开发过程中,要有多线程并发的思维,从并发的角度去思考,再结合资源有序分配原则,就可以大大避免死锁问题的发生。
- 技能知识点
- 对死锁问题的理解
- 文件系统原理:如何用1分钟遍历一个100TB的文件?
- 数据库原理:为什么PrepareStatement性能更好更安全?
- Java Web程序的运行时环境到底是怎样的?
- 你真的知道自己要解决的问题是什么吗?
- 如何解决问题
- 经验分享
- GIT的HTTP方式免密pull、push
- 使用xhprof对php7程序进行性能分析
- 微信扫码登录和使用公众号方式进行扫码登录
- 关于curl跳转抓取
- Linux 下配置 Git 操作免登录 ssh 公钥
- Linux Memcached 安装
- php7安装3.4版本的phalcon扩展
- centos7下php7.0.x安装phalcon框架
- 将字符串按照指定长度分割
- 搜索html源码中标签包的纯文本
- 更换composer镜像源为阿里云
- mac 隐藏文件显示/隐藏
- 谷歌(google)世界各国网址大全
- 实战文档
- PHP7安装intl扩展和linux安装icu
- linux编译安装时常见错误解决办法
- linux删除文件后不释放磁盘空间解决方法
- PHP开启异步多线程执行脚本
- file_exists(): open_basedir restriction in effect. File完美解决方案
- PHP 7.1 安装 ssh2 扩展,用于PHP进行ssh连接
- php命令行加载的php.ini
- linux文件实时同步
- linux下php的psr.so扩展源码安装
- php将字符串中的\n变成真正的换行符?
- PHP7 下安装 memcache 和 memcached 扩展
- PHP 高级面试题 - 如果没有 mb 系列函数,如何切割多字节字符串
- PHP设置脚本最大执行时间的三种方法
- 升级Php 7.4带来的两个大坑
- 不同域名的iframe下,fckeditor在chrome下的SecurityError,解决办法~~
- Linux find+rm -rf 执行组合删除
- 从零搭建Prometheus监控报警系统
- Bug之group_concat默认长度限制
- PHP生成的XML显示无效的Char值27消息(PHP generated XML shows invalid Char value 27 message)
- XML 解析中,如何排除控制字符
- PHP各种时间获取
- nginx配置移动自适应跳转
- 已安装nginx动态添加模块
- auto_prepend_file与auto_append_file使用方法
- 利用nginx实现web页面插入统计代码
- Nginx中的rewrite指令(break,last,redirect,permanent)
- nginx 中 index try_files location 这三个配置项的作用
- linux安装git服务器
- PHP 中运用 elasticsearch
- PHP解析Mysql Binlog
- 好用的PHP学习网(持续更新中)
- 一篇写给准备升级PHP7的小伙伴的文章
- linux 安装php7 -系统centos7
- Linux 下多php 版本共存安装
- PHP编译安装时常见错误解决办法,php编译常见错误
- nginx upstream模块--负载均衡
- 如何解决Tomcat服务器打开不了HOST Manager的问题
- PHP的内存泄露问题与垃圾回收
- Redis数据结构 - string字符串
- PHP开发api接口安全验证
- 服务接口API限流 Rate Limit
- php内核分析---内存管理(一)
- PHP内存泄漏问题解析
- 【代码片-1】 MongoDB与PHP -- 高级查询
- 【代码片-1】 php7 mongoDB 简单封装
- php与mysql系统中出现大量数据库sleep的空连接问题分析
- 解决crond引发大量sendmail、postdrop进程问题
- PHP操作MongoDB GridFS 存储文件,如图片文件
- 浅谈php安全
- linux上keepalived+nginx实现高可用web负载均衡
- 整理php防注入和XSS攻击通用过滤