java并发编程系列——死锁的解决
Posted 守夜人爱吃兔子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发编程系列——死锁的解决相关的知识,希望对你有一定的参考价值。
接下来我会以一个经典的转帐问题,跟大家一起聊聊死锁,以及如何解决这个问题。
假如客户 A 需要给客户 B 转账,客户 A 的账户减少 100 元,客户 B 的账户增加 100 元。
我们转化为代码描述:有一个账户类 Account,账户类有一个转账方法 transfer() 方法,该方法接收两个参数,转入账户和转账金额。
示例:1
public class Account {
private int balance;
public void transfer(Account target,int amt) {
this.balance -= amt;
target.balance += amt;
}
}
实例 1 中有一个成员变量 balance,如何保证该成员变量在并发情况下没有线程安全性问题呢?我相信经过你的思考你会对代码做如下改进:
示例:2
public class Account {
private int balance;
public synchronized void transfer(Account target,int amt) {
this.balance -= amt;
target.balance += amt;
}
}
那么加上 Synchronized 的方法有啥问题呢?我们在前面已经学习过了这种非静态方法,默认的锁是 this 也就是当前对象,但是如果我们仔细观察这段代码我们就会发现问题出在哪,代码区有两个资源,一个 this 表示转出账户,target 表示转入账户。而锁(this)只能锁住当前转出账户的变量 balance,无法锁住转入账户的 balance,所以此时如果有线程操作了账户 B(比如客户 B 取了 100 元)。
既然我们知道问题出现在哪了,当前转出账户的锁不能锁住两个资源,那么我们是不是可以用多把锁去锁住两个资源呢。
示例:3
public class Account {
private int balance;
public void transfer(Account target,int amt) {
synchronized(this) {
synchronized(target) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
看上去我们似乎解决 this 这把锁不能锁住两个资源的情况,确实我们解决了这个问题,但是新的问题又出现了。我们假设这样的场景,A 给 B 转账的同时,B 也在给 A 转账。
对应到代码层面的逻辑:
- A 给 B 转账:首先获取对象 A 的锁,然后尝试去获取对象 B 的锁
- B 给 A 转账:首先获取对象 B 的锁,然后尝试获取对象 A 的锁
此时我们可以看出,形成了死锁,A 转账时,拿了自己的锁尝试去获取 B 的锁,B 转账时拿了自己的锁尝试去获取 A 的锁,然后谁也拿不到对方的锁,这就是死锁。
死锁预防
在 Java 并发编程领域已经有技术大咖总结出了发生死锁的条件,只有四个条件都发生时才会出现死锁:
- 互斥,共享资源 X 和 Y 只能被一个线程占用
- 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X
- 不可抢占,其他线程不能强行抢占线程 T1 占有的资源
- 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待
只要能破坏其中一个,就可以成功避免死锁的发生,因为对于共享资源我们要得就是互斥的效果,所以第一个条件是无法破坏的,所以可以从下面三个条件出手,具体实现方式如下。
解决该死锁问题
调整获取锁的顺序
通过上面死锁分析,我们知道因为同时转账时,获取锁的顺序不一致导致了死锁问题,那么我们可以通过调整获取锁的顺序保持一致,那么死锁问题也就可以解决了。
- A 转账给 B:A 获取自身的锁,再尝试获取 B 锁
- B 转账给 A:B 首先去获取 A 的锁,再尝试获取 B 自己的锁
每个账户都有唯一的 Id,通常情况 id 也是递增的,我们可以通过 id 的大小来获取锁的顺序。
代码如下:
示例:4
public class Account {
private int balance;
private int id;
public void transfer(Account target,int amt) {
Account first = this;
Account second = target;
if(this.id < target.id) {
first = target;
second = this;
}
synchronized(first) {
synchronized(second ) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
我们通过调整获取锁的顺序,每次获取的锁都是 id 最大的,这样就能保证获取锁的顺序问题,从而避免了死锁。
假设账户 A 的 id 为 100,账户 B 的 id 为 101;
- A 转账给 B:B.id>A.id 首先获取 B 的锁,然后尝试获取 A 的锁
- B 转账给 A:B.id > A.id 首先尝试获取 B 的锁(等待)
设置超时放弃
我们在上面的编码中使用的是 Synchronized 内置锁,无法设置超时,如果需要设置超时我们可是使用显示锁 Lock。
示例:5
private Lock lock = new ReentrantLock();
private int balance;
public void transfer(Account target, int amt) {
try {
lock.tryLock(10, TimeUnit.MILLISECONDS);
this.balance -= amt;
target.balance += amt;
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
}
该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。
注:实际开发中这种都是用数据库事务 + 乐观锁的方式解决的。本例子只是为了演示死锁。
最后
一直想整理出一份完美的面试宝典,但是时间上一直腾不开,这套一千多道面试题宝典,结合今年金三银四各种大厂面试题,以及 GitHub 上 star 数超 30K+ 的文档整理出来的,我上传以后,毫无意外的短短半个小时点赞量就达到了 13k,说实话还是有点不可思议的。
需要完整版的小伙伴,可以一键三连后,点击这里!
一千道互联网 Java 工程师面试题
内容涵盖:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、mysql、Spring、SpringBoot、SpringCloud、RabbitMQ、Kafka、Linux等技术栈(485页)
《Java核心知识点合集(283页)》
内容涵盖:Java基础、JVM、高并发、多线程、分布式、设计模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat、数据库、云计算等
《Java中高级核心知识点合集(524页)》
《Java高级架构知识点整理》
由于篇幅限制,详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
需要完整版的小伙伴,可以一键三连后,点击这里!
以上是关于java并发编程系列——死锁的解决的主要内容,如果未能解决你的问题,请参考以下文章