您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
首页
博文
课程
大赛
工具
更多
用户中心
开发者社区
>
博文
>
Java并发编程之死锁
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
Java并发编程之死锁
自猿其说Tech
2022-05-11
IP归属:未知
20880浏览
计算机编程
### 1 前言 在多线程编程中,我们经常会遇到多个线程访问同一个共享资源的情况,这个时候必须考虑如何维护数据一致性。那么为了保证数据操作的一致性,操作系统引入了锁机制,用于保证临界区资源的安全。通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区,从而保证临界区中操作数据的一致性。但是过渡或是不规范的使用锁,很有可能会导致死锁现象,一旦产生死锁,就会造成系统功能不可用。 ### 2 什么是死锁 死锁是一种特定的程序状态,主要是由于循环依赖导致彼此一直处于等待中,而使得程序陷入僵局。死锁不仅仅发生在线程之间,而对于资源独占的进程之间同样可能出现死锁。通常来说,我们所说的死锁,是指两个或多个线程之间,由于互相持有对方所需要的锁,进而产生永久阻塞的情况。 先看一个很简单的死锁例子:线程A先获取到了锁A,然后等待获取锁B,线程B先获取到了锁B,然后等待锁A,那么线程A和线程B,互相等待对方释放锁,相持不下,陷入一直等待(如图)。当然现实中产生死锁要比这种情况复杂得多。 ![](//img1.jcloudcs.com/developer.jdcloud.com/b707e807-0152-4798-aa9b-b735d3e8d3d420220511160811.png) #### 2.1 产生死锁的原因 主要以下3点: 1. 因为系统资源不足。 2. 进程运行推进的顺序不合适. 3. 资源分配不当等。 系统中的资源可以分成两类: 1. 可剥夺资源:是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源; 2. 不可剥夺资源:当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。 那么竞争资源中导致死锁发生的原因: 如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。比如对于不可剥夺资源,当资源占用时,其余想要使用该资源的都要进行等待。其次,进程运行推进顺序与速度不同,也可能产生死锁。 看一个死锁代码: ```java public static void main(String[] args) { String lock1 = "A锁"; String lock2 = "B锁"; new Thread(()->{ synchronized (lock1){ System.out.println(Thread.currentThread().getName()+"获取A锁,尝试获取B锁"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2){ System.out.println(Thread.currentThread().getName()+"获取A锁和B锁"); } } },"线程一").start(); new Thread(()->{ synchronized (lock2){ System.out.println(Thread.currentThread().getName()+"获取B锁,尝试获取A锁"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1){ System.out.println(Thread.currentThread().getName()+"获取A锁和B锁"); } } },"线程二").start(); } ``` 此刻,根据前面对于死锁的了解可知,线程一在尝试获取B锁并持有A锁,线程二在尝试获取A锁并持有B锁,线程一和二都获取不到接下来想要获取到的锁,程序将一致保持尝试获取锁的状态。 ![](//img1.jcloudcs.com/developer.jdcloud.com/e0eee101-1d7c-41dd-a355-52bf59e6512f20220511160956.png) #### 2.2 产生死锁的4个必要条件 1. 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。 2. 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。 3. 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。 4. 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。 #### 2.3 如何预防死锁 避免死锁其实就是从破坏死锁的4个必要条件入手。 1. 破坏互斥条件:即允许进程同时访问某些资源。但这个对于一些资源来说由于本身的设计导致无法破坏互斥条件,因此不会去破坏这个条件。 2. 破坏请求和保持条件:一次性分配所有资源,这样就不会再有请求了。 3. 破坏不可剥夺资源:当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源。 4. 破坏环路等待条件:资源有序分配法,系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反 #### 2.4 避免死锁 通过预发死锁的四个破坏,可以尽量的预防死锁的产生。避免死锁是在系统运行过程中注意避免死锁的最终发生。 可以通过以下方法来避免死锁: 1)加锁顺序(线程按照一定的顺序加锁) 根据上面产生死锁的例子,调整一下线程获取锁的顺序:将线程二修改为先获取锁A,再获取锁B得到的结果是 ![](//img1.jcloudcs.com/developer.jdcloud.com/fd0923eb-613f-4052-b5a7-18e6cb242ceb20220511162251.png) 2)加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁) 通过Lock接口提供的boolean tryLock(long time, TimeUnit unit)方法来设置等待时间,超过时间后对以获取到的锁进行。 ``` public static void main(String[] args) { Lock lockA = new ReentrantLock(); Lock lockB = new ReentrantLock(); new Thread(()->{ lockA.lock(); System.out.println(Thread.currentThread().getName()+"lockA锁住,尝试锁住lockB"); try { if (lockB.tryLock(1,TimeUnit.SECONDS)){ lockB.lock(); System.out.println(Thread.currentThread().getName()+"锁住lockA,lockB"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lockA.unlock(); System.out.println("释放lockA"); } },"线程一").start(); new Thread(() -> { lockB.lock(); System.out.println(Thread.currentThread().getName() + "lockB锁住,尝试锁住lockA"); lockA.lock(); System.out.println(Thread.currentThread().getName() + "锁住lockA,lockB"); }, "线程二").start(); ``` 可以看到当线程一锁住lockA后尝试获取lockB,此时线程二锁住lockB,尝试锁住lockA,在相互等待过程中线程一获取lockB超过等待时长,释放lockA,线程二往下继续。 ![](//img1.jcloudcs.com/developer.jdcloud.com/ec6dc6fc-d514-4fda-b11d-cab8d8294b4d20220511162345.png) 3)死锁检测 主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景 ### 3 死锁检测 #### 3.1 jstack命令 jstack用于生成虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。当线程出现停顿时通过jstack来查看各个线程调用堆栈,可以得知在线程在做些什么 jps用来展示当前系统所有Java进程情况以及pid。 先用jps找到运行得Java进程。 ![](//img1.jcloudcs.com/developer.jdcloud.com/2d59b27a-8c77-4209-8c2c-2db6084cb2b420220511162433.png) 再用jstack查看线程 在使用这个命令前先了解一下jstack如何展示线程的不同状态: NEW,未启动的。不会出现在Dump中。 RUNNABLE,在虚拟机内执行的。 BLOCKED,受阻塞并等待监视器锁。 WATING,无限期等待另一个线程执行特定操作。 TIMED_WATING,有时限的等待另一个线程的特定操作。 TERMINATED,已退出的。 根据展示出的线程信息可以直观看出程序发生死锁,found one java-level deadLock,线程二再等待获取的被线程一持有,线程一等待的被线程二持有。 ![](//img1.jcloudcs.com/developer.jdcloud.com/5e8ee535-6f04-45c4-83ca-1cafdbf065a220220511162453.png) #### 3.2 Jconsole与Jvisualvm JConsole 是一个内置 Java 性能分析器。我们可以JConsole(或者,更高端的升级版jvisualvm)来监控 Java 应用程序性能和跟踪 Java 中的代码,包括死锁检测。 控制台输入Jconsole会弹出窗口,选择对应线程,连接进去后选择线程-死锁检测,会检测出发送死锁的线程 ![](//img1.jcloudcs.com/developer.jdcloud.com/54a49fb0-01c5-4c3b-bfab-54c920a1770d20220511162516.png) 输入jvisualvm点入相应线程,生成线程Dump,会展示发生死锁的具体内容。 ![](//img1.jcloudcs.com/developer.jdcloud.com/da9aa681-6621-48f2-8327-6b59b82fd27c20220511162607.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/1faf5650-80ba-4d43-9fe6-aa8b636dc9f920220511162619.png) ### 4 死锁恢复 1)进程终止: - 终止所有死锁进程(开销大,消耗资源多) - 一个一个终止进程,直到死锁现象消失。 - 终止的顺序可以根据三个方面考虑:优先级,需要的资源,运行的时间 2)资源抢占:从死锁的环结构中抢占资源,直到其循环被打破 - 选择牺牲品:抢占谁的资源,这需要综合考虑 - 回滚:必须将被抢占资源的进程回滚到安全状态 - 饥饿:同一个进程所持有的资源很可能每次都被抢占,导致这个线程一直执行不完。所以要确保一个进程被选为牺牲品有次数限制 ### 5 总结 根据上面的了解,在实际应用中,可以从以下几方面规避死锁的问题,毕竟死锁一旦产生就会造成不可逆的后果 1. 合理的思考以及设计加锁的顺序。 2. 对于等待锁的线程设置一定等待时间,避免一直等待。 3. 减少锁住的范围,来减少锁阻塞的情况 降低锁的粒度 使用同步代码块,减少使用同步方法 专锁专用 4. 尽量不自己去设计锁,可以使用JUC提供的并发类 5. 避免锁的嵌套,一旦获取顺序反了就会陷入死锁 6. 分配资源前看能不能收回资源 除此之外,一旦发生死锁,就需要去逐一排查,因此给线程起一个有意义的名字也是很重要的,方便更好的定位问题,并且编程中对于字段定义一个合适的名字也便于开发理解。 ------------ ###### 自猿其说Tech-JDL京东物流技术与数据智能部 ###### 作者:曹曦予
原创文章,需联系作者,授权转载
上一篇:CompletableFuture探索之旅
下一篇:BPO清铢产品设计思考
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149946
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149946
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号