详解java中产生死锁的原因及如何避免
作者:yehao_123456 发布时间:2022-04-22 00:36:14
1. Java中导致死锁的原因
Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。
从这两个例子,我们可以得出结论,产生死锁可能性的最根本原因是:线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,也就是说在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。另一个原因是默认的锁申请操作是阻塞的。
2. Java中如何避免死锁
既然我们知道了产生死锁可能性的原因,那么就可以在编码时进行规避。Java是面向对象的编程语言,程序的最小单元是对象,对象封装了数据和操作,所以Java中的锁一般也是以对象为单位的,对象的内置锁保护对象中的数据的并发访问。所以如果我们能够避免在对象的同步方法中调用其它对象的同步方法,那么就可以避免死锁产生的可能性。如下所示的代码,就存在死锁的可能性:
public class ClassB {
private String address;
// ...
public synchronized void method1(){
// do something
}
// ... ...
}
public class ClassA {
private int id;
private String name;
private ClassB b;
// ...
public synchronized void m1(){
// do something
b.method1();
}
// ... ...
}
上面的ClassA.m1()方法,在对象的同步方法中又调用了ClassB的同步方法method1(),所以存在死锁发生的可能性。我们可以修改如下,避免死锁:
public class ClassA {
private int id;
private String name;
private ClassB b;
// ...
public void m2(){
synchronized(this){
// do something
}
b.method1();
}
// ... ...
}
这样的话减小了锁定的范围,两个锁的申请就没有发生交叉,避免了死锁的可能性,这是最理性的情况,因为锁没有发生交叉。但是有时是不允许我们这样做的。此时,如果只有ClassA中只有一个m1这样的方法,需要同时获得两个对象上的锁,并且不会将实例属性 b 溢出(return b;),而是将实例属性 b 封闭在对象中,那么也不会发生死锁。因为无法形成死锁的闭环。但是如果ClassA中有多个方法需要同时获得两个对象上的锁,那么这些方法就必须以相同的顺序获得锁。
比如银行转账的场景下,我们必须同时获得两个账户上的锁,才能进行操作,两个锁的申请必须发生交叉。这时我们也可以打破死锁的那个闭环,在涉及到要同时申请两个锁的方法中,总是以相同的顺序来申请锁,比如总是先申请 id 大的账户上的锁 ,然后再申请 id 小的账户上的锁,这样就无法形成导致死锁的那个闭环。
public class Account {
private int id; // 主键
private String name;
private double balance;
public void transfer(Account from, Account to, double money){
if(from.getId() > to.getId()){
synchronized(from){
synchronized(to){
// transfer
}
}
}else{
synchronized(to){
synchronized(from){
// transfer
}
}
}
}
public int getId() {
return id;
}
}
这样的话,即使发生了两个账户比如 id=1的和id=100的两个账户相互转账,因为不管是哪个线程先获得了id=100上的锁,另外一个线程都不会去获得id=1上的锁(因为他没有获得id=100上的锁),只能是哪个线程先获得id=100上的锁,哪个线程就先进行转账。这里除了使用id之外,如果没有类似id这样的属性可以比较,那么也可以使用对象的hashCode()的值来进行比较。
上面我们说到,死锁的另一个原因是默认的锁申请操作是阻塞的,所以如果我们不使用默认阻塞的锁,也是可以避免死锁的。我们可以使用ReentrantLock.tryLock()方法,在一个循环中,如果tryLock()返回失败,那么就释放以及获得的锁,并睡眠一小段时间。这样就打破了死锁的闭环。
比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1
此时如果T3申请锁L1失败,那么T3释放锁L3,并进行睡眠,那么T2就可以获得L3了,然后T2执行完之后释放L2, L3,所以T1也可以获得L2了执行完然后释放锁L1, L2,然后T3睡眠醒来,也可以获得L1, L3了。打破了死锁的闭环。
这些情况,都还是比较好处理的,因为它们都是相关的,我们很容易意识到这里有发生死锁的可能性,从而可以加以防备。很多情况的场景都不会很明显的让我们察觉到会存在发生死锁的可能性。所以我们还是要注意:
一旦我们在一个同步方法中,或者说在一个锁的保护的范围中,调用了其它对象的方法时,就要十而分的小心:
1)如果其它对象的这个方法会消耗比较长的时间,那么就会导致锁被我们持有了很长的时间;
2)如果其它对象的这个方法是一个同步方法,那么就要注意避免发生死锁的可能性了;
最好是能够避免在一个同步方法中调用其它对象的延时方法和同步方法。如果不能避免,就要采取上面说到的编码技巧,打破死锁的闭环,防止死锁的发生。同时我们还可以尽量使用“不可变对象”来避免锁的使用,在某些情况下还可以避免对象的共享,比如 new 一个新的对象代替共享的对象,因为锁一般是对象上的,对象不相同了,也就可以避免死锁,另外尽量避免使用静态同步方法,因为静态同步相当于全局锁。还有一些封闭技术可以使用:比如堆栈封闭,线程封闭,ThreadLocal,这些技术可以减少对象的共享,也就减少了死锁的可能性。
总结一下:
死锁的根本原因
1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;
2)默认的锁申请操作是阻塞的。
所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。要采取各种方法来杜绝这种可能性。
以上所述是小编给大家介绍的java中产生死锁的原因及如何避免详解整合网站的支持!
来源:https://blog.csdn.net/m0_38126177/article/details/78587845


猜你喜欢
- 本文实例为大家分享了java实现通过绑定邮箱找回密码功能,供大家参考,具体内容如下1.输入用户名及验证码,验证用户名是否存在(1).生成验证
- 前言本文将提供一个redis的工具类,可以用在Spring boot以及Spring Cloud项目中,本工具类主要整合了将Redis作为N
- 先给大家展示下效果图:不知道大家对效果图感觉怎么样,个人觉还不错,感兴趣的朋友可以参考下实现代码哦。public class ToggleB
- 在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发
- 线上出现了如上的 crash,第一解决反应是在 show dialog 之前做个 isFinish 和 isDestroyed 判断,当我翻
- 本文实例讲述了android从系统图库中取图片的实现方法。分享给大家供大家参考。具体如下:在自己应用中,从系统图库中取图片,然后截取其中一部
- global 是 C# 2.0 中新增的关键字,理论上说,如果代码写得好的话,根本不需要用到它。假设你现在写了一个类,名字叫 System。
- 第一部分: 使用idea 打包工程jar 1.准备好一份 开发好的 可执行的 含有main方法的&nbs
- 前言aop面向切面编程,是编程中一个很重要的思想本篇文章主要介绍的是SpringBoot切面Aop的使用和案例什么是aopAOP(Aspec
- Kotlin中函数都是头等的,这意味着它可以存储在变量与数据结构中、作为参数传递给其他高阶函数以及从其他高阶函数返回。可以向操作任何其他非函
- 效果视频引用描述本示例采用的是非常、非常、非常好用的一款第三方SDK——helloCharts传送门导包第一步 :导入mavenmaven
- CancellationTokenCancellationToken有一个构造函数,可以传入一个bool类型表示当前的Cancellatio
- 在上一篇文章《驱动开发:内核字符串转换方法》中简单介绍了内核是如何使用字符串以及字符串之间的转换方法,本章将继续探索字符串的拷贝与比较,与应
- 本文实例为大家分享了Android实现拼图小游戏的具体代码,供大家参考,具体内容如下目标效果: 1.activity_main.x
- Java Tess4J实现图像识别最近需要用Java做一个图像识别的东西,查了一些资料,在此写一个基于Tess4J的教程,方便其他人参考和使
- ////////////////////////////
- 项目结构项目路径可以自己定义,只要路径映射正确就可以pom.xml <properties> <spring.versio
- Object(四大方法):文章干货满满,耐性看完~~何为Object?首先先来看看官方对Object的介绍:在这里附上Java官方的查阅工具
- 面试中会经常遇到手撕代码的情况,而求TopK的是经常遇到的题目。下面我就用Java来实现。主要通过两种方法实现,快排思想以及堆排序的思想,两
- 一. String对象的比较1. ==比较是否引用同一个对象注意:对于内置类型,==比较的是变量中的值;对于引用类型 , == 比较的是引用