并发与多线程【二】——死锁
引言
多线程协作时,因为对资源的锁定与等待会产生死锁,在明白死锁概念后需要了解死锁产生的四个基本条件,明白竞争条件和临界区的概念,还需要知道通过破坏造成死锁的4个条件来防止死锁。
下面对死锁概念、死锁的产生原因、死锁的四个必要条件等内容作记录。
在描述死锁概念之前先理解竞态条件和临界区两个概念。
竞态条件
静态条件:如果程序运行顺序的改变会影响最终结果,这就是一个竞态条件(race condition)。从这句话来看“竞态条件”似乎是一种“现象”——由于程序运行顺序的不同造成得到最终结果的不同这样的一种现象,称这种现象为“竞态条件”。
举个被大家用烂了的例子:
如果一段程序运行多次的结果不一致(排除生成随机数的情况),那这就可能是竞态条件的体现。比如最典型的例子,两个线程同时把一个类的静态成员做50次自增加1的操作,即:
i++
写在两个线程中,都运行50次,运行结束以后用主线程去取这个变量的值几乎不可能是100. 有的时候是97,有的时候是98,这是用来说明竞态条件的最有效例子。
自增加操作其实是三个操作的组合:
- 取该变量的值
- 给这个取到的值+1
- 把计算好的值赋给该变量
学过计算机系统的同学会知道这三个操作的区别,取值是从内存取到寄存器,+1以后值还是在寄存器,只有在赋值完成后,内存中的该变量的值才会变化。
我们的竞态条件发生的原因就是在一个+1的值还没有赋给变量的时候,另一个线程开始读取内存中的该变量的值,等这个线程完成+1、赋值以后,他的工作其实和之前那个线程是一样的,有若干类似现象出现以后,就会导致最后的值永远达不到100。
临界区
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
导致竞态条件发生的代码区称作临界区。就是说临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。
什么是死锁?
深入理解计算机系统书中是这样定义死锁的:信号量引入了一种潜在的运行时错误,叫做死锁(deadlock),它指的是一组线程被阻塞了,等待一个永远也不会为真的条件。并用进度图对死锁进行了描述和讲解。
死锁 : 当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
死锁产生的四个必要条件
注意是必要条件。
互斥条件
进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件(占有且等待条件)
当进程因请求资源而阻塞时,对已获得的资源保持不放。
不可剥夺条件
进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
环路等待条件
在发生死锁时,必然存在一个进程--资源的环形链。
如何检测死锁?
要想检测死锁,先得创造一个死锁。
package com.xgcd.concurrent; public class DeadLockTest { public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { DeadLockTest.method1(); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { DeadLockTest.method2(); } }); t1.start(); t2.start(); } public static void method1() { // synchronized(lockA) synchronized (String.class) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程a尝试获取Integer.class..."); // synchronized(lockB) synchronized (Integer.class) { System.out.println("线程a获取Integer.class success..."); } } } public static void method2() { synchronized (Integer.class) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程b尝试获取String.class..."); synchronized (String.class) { System.out.println("线程b获取String.class success..."); } } } }
运行main方法后,执行结果:
1、JConsole工具
此时通过 JDK 自带的工具 jConsole 来检测死锁的存在,命令行中输入 jconsole,调出工具:
2、Jstack工具
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。
Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿
的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
如何防止死锁?
防止死锁或者说确保系统永远不会进入死锁状态,产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
破坏“占有且等待”条件
方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。
优点:简单易实施且安全。
缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。使进程经常发生饥饿现象。
方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。
破坏“不可抢占”条件
当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。
该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。
破坏“循环等待”条件
可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为 i 的资源时,那么它下一次申请资源只能申请编号大于 i 的资源。这样虽然避免了循环等待,但是这种方法是比较低效的,资源的执行速度回变慢,并且可能在没有必要的情况下拒
绝资源的访问,比如说,进程 c 想要申请资源1,如果资源1并没有被其他进程占有,此时将它分配个进程 c 是没有问题的,但是为了避免产生循环等待,该申请会被拒绝,这样就降低了资源的利用率。
如何解除死锁?
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
剥夺资源
从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态。
撤消进程
可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止。所谓代价是指优先级、运行代价、进程的重要性和价值等。
感谢
-
https://blog.csdn.net/Clifnich/article/details/78447524
-
https://blog.csdn.net/guaiguaihenguai/article/details/80303835
-
https://blog.csdn.net/jonnyhsu_0913/article/details/79633656
-
https://blog.csdn.net/hd12370/article/details/82814348
作者:习惯沉淀
如果文中有误或对本文有不同的见解,欢迎在评论区留言。
如果觉得文章对你有帮助,请点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
扫码关注一线码农的学习见闻与思考。
回复"大数据","微服务","架构师","面试总结",获取更多学习资源!