产生这种情况的原因,是不同的线程通过不同顺序去获取相同的锁;比如线程1获取锁的顺序是left -> right,而线程2获取锁的顺序是right -> left,在某种情况下会发生死锁。拿上面的案例分析,我们通过Java自带的jps和jstack工具查看java进程ID和线程相关信息。
jps查看LeftRightDeadLock的进程id为17968
jstack查看进程中的线程信息,线程信息比较多,我把重要的复制出来,如下的图中能很明显的看到产生了死锁。
这里省略了很多线程当前状态信息
解决顺序死锁的办法其实就是保证所有线程以相同的顺序获取锁就行。
3.2 动态锁顺序死锁
3.2.1 动态锁顺序死锁的产生与示例
动态锁顺序死锁与上面的锁顺序死锁其实最本质的区别,就在于动态锁顺序死锁锁住的资源无法确定或者会发生改变。
比如说银行转账业务中,账户A向账户B转账,账户B也可以向账户A转账,这种情况下如果加锁的方式不正确就会发生死锁,比如如下代码:
定义简单的账户类Account
package com.liziba.dl;
import java.math.BigDecimal;
/**
-
-
账户类
-
@Author: Liziba
*/
public class Account {
/** 账户 */
public String number;
/** 余额 */
public BigDecimal balance;
public Account(String number, BigDecimal balance) {
this.number = number;
this.balance = balance;
}
public void setNumber(String number) {
this.number = number;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
}
定义转账类TransferMoney,其中有transferMoney()方法用于accountFrom账户向accountTo转账金额amt:
package com.liziba.dl;
import java.math.BigDecimal;
/**
-
-
转账类
-
@Author: Liziba
*/
public class TransferMoney {
/**
-
转账方法
-
@param accountFrom 转账方
-
@param accountTo 接收方
-
@param amt 转账金额
-
@throws Exception
*/
public static void transferMoney(Account accountFrom,
Account accountTo,
BigDecimal amt) throws Exception {
synchronized (accountFrom) {
synchronized (accountTo) {
BigDecimal formBalance = accountFrom.balance;
if (formBalance.compareTo(amt) < 0) {
throw new Exception(accountFrom.number + " balance is not enough.");
} else {
accountFrom.setBalance(formBalance.subtract(amt));
accountTo.setBalance(accountTo.balance.add(amt));
System.out.println(“Form” + accountFrom.number + ": " + accountFrom.balance.toPlainString()
+“\t” + “To” + accountTo.number + ": " + accountTo.balance.toPlainString());
}
}
}
}
}
上面这个类看似规定了锁的顺序由accountFrom到accountTo不会产生死锁,但是这个accountFrom和accountTo是由调用方来传入的,当A向B转账时accountFrom = A,accountTo = B;当B向A转账时accountFrom = B,accountTo = A;假设两者在同一时刻给对方发起转账,则仍然存在3.1中锁顺序死锁问题。比如如下测试:
public static void main(String[] args) {
// 账户A && 账户B
Account accountA = new Account(“111111”, new BigDecimal(10000));
Account accountB = new Account(“2222222”, new BigDecimal(10000));
// 循环创建线程 A -> B ; B -> A 各一百个线程
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
// 转账顺序 A -> B
transferMoney(accountA, accountB, new BigDecimal(10));
} catch (Exception e) {
return;
}
}).start();
new Thread(() -> {
try {
// 转账顺序 B -> A
transferMoney(accountB, accountA, new BigDecimal(10));
} catch (Exception e) {
return;
}
}).start();
}
}
程序执行无法正确结束,如下所示:
依然使用jps+ jstack查看这个java进程的线程信息,发现Thread-89和Thread-90之间产生死锁
3.2.2 动态锁顺序死锁的解决
解决动态锁顺序死锁的办法,就是通过一定的手段来严格控制加锁的顺序。比如通过对象中某一个唯一的属性值比如id;或者也可以通过对象的散列值+hash冲突解决来控制加锁的顺序。
我们通过对象的散列值+hash冲突解决的方式来优化上面的代码:
package com.liziba.dl;
import java.math.BigDecimal;
/**
-
-
转账类优化 -> 通过hash算法
-
@Author: Liziba
*/
public class TransferMoneyOptimize {
/** hash 冲突时使用第三个锁(优秀的hash算法冲突是很少的!) */
private static final Object conflictShareLock = new Object();
/**
-
转账方法
-
@param accountFrom 转账方
-
@param accountTo 接收方
-
@param amt 转账金额
-
@throws Exception
*/
public static void transferMoney(Account accountFrom,
Account accountTo,
BigDecimal amt) throws Exception {
// 计算hash值
int accountFromHash = System.identityHashCode(accountFrom);
int accountToHash = System.identityHashCode(accountTo);
// 如下三个分支能一定控制账户之间的转是不会产生死锁的
if (accountFromHash > accountToHash) {
synchronized (accountFrom) {
synchronized (accountTo) {
transferMoneyHandler(accountFrom, accountTo, amt);
}
}
} else if (accountToHash > accountFromHash) {
synchronized (accountTo) {
synchronized (accountFrom) {
transferMoneyHandler(accountFrom, accountTo, amt);
}
}
} else {
// 解决hash冲突
synchronized (conflictShareLock) {
synchronized (accountFrom) {
synchronized (accountTo) {
transferMoneyHandler(accountFrom, accountTo, amt);
}
}
}
}
}
/**
-
账户金额增加处理
-
@param accountFrom 转账方
-
@param accountTo 接收方
-
@param amt 转账金额
-
@throws Exception
*/
private static void transferMoneyHandler(Account accountFrom,
Account accountTo,
BigDecimal amt) throws Exception {
if (accountFrom.balance.compareTo(amt) < 0) {
throw new Exception(accountFrom.number + " balance is not enough.");
} else {
accountFrom.setBalance(accountFrom.balance.subtract(amt));
accountTo.setBalance(accountTo.balance.add(amt));
System.out.println(“Form” + accountFrom.number + ": " + accountFrom.balance.toPlainString()
+“\t” + “To” + accountTo.number + ": " + accountTo.balance.toPlainString());
}
}
}
测试代码与上面错误的示例代码一致,经过数次其输出结果均为如下:
在上面两种死锁的产生原因都是因为两个线程以不同的顺序获取相同的所导致的,而解决的办法都是通过一定的规范来严格控制加锁的顺序,这样就能正确的规避死锁的风险。
3.3 协作对象之间的死锁
3.3.1 协作对象死锁的产生与示例
死锁的产生往往没有上述两种死锁产生的那么明显,就算其存在死锁风险也只有在高并发的场景下才会暴露出来(这并不意味着没得高并发的应用就不用考虑死锁问题了啊,弟兄们!)。如下介绍一种隐藏的比较深的死锁,这种死锁产生在多个协作对象的函数调用不透明。
如下以出租车为例介绍协作对象之间死锁的产生,其主要涉及到以下几个类(省略了很多代码,自行脑补哈!):
-
Coordinate -> 坐标类,出租车经纬度信息类
-
Taxi -> 出租车类,出租车所属于某个出租车车队Fleet,此外包含当前坐标location和目的地坐标destination,出租车在更新目的地信息的时候会判断当前坐标与目的地坐标是否相等,相等则会通知所属车队车辆空闲,可以接收下一个目的地
-
Fleet -> 出租车车队类,出租车类包含两个集合taxis和available,分别用来保存车队中所有车辆信息和车队中当前空闲的出租车信息,此外提供获取车队中所有出租车当前地址信息的快照方法getImage()
-
Image -> 车辆地址信息快照类,用于获取出租车的地址信息
Coordinate(坐标类) 代码示例:
package com.liziba.dl;
/**
-
-
坐标类
-
@Author: Liziba
*/
public class Coordinate {
/** 经度 */
private Double longitude;
/** 纬度 */
private Double latitude;
// 省略 getXxx,setXxx等方法
}
Taxi(出租车类)代码示例;
package com.liziba.dl;
import java.util.Objects;
/**
-
-
出租车类
-
@Author: Liziba
*/
public class Taxi {
/** 出租车唯一标志 */
private String id;
/** 当前坐标 */
private Coordinate location;
/** 目的地坐标 */
private Coordinate destination;
/** 所属车队 */
private final Fleet fleet;
/**
-
获取当前地址信息
-
@return
*/
public synchronized Coordinate getLocation() {
return location;
}
/**
-
更新当前地址信息
-
如果当前地址与目的地地址一致,则表名到达目的地需要通知车队,当前出租车空闲可用前往下一个目的地
-
@param location
*/
public synchronized void setLocation(Coordinate location) {
this.location = location;
if (location.equals(destination)) {
fleet.free(this);
}
}
public Coordinate getDestination() {
return destination;
}
/**
-
设置目的地
-
@param destination
*/
public synchronized void setDestination(Coordinate destination) {
this.destination = destination;
}
public Taxi(Fleet fleet) {
this.fleet = fleet;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Taxi taxi = (Taxi) o;
return Objects.equals(location, taxi.location) &&
Objects.equals(destination, taxi.destination);
}
@Override
public int hashCode() {
return Objects.hash(location, destination);
}
}
Fleet(出租车车队类)示例代码:
package com.liziba.dl;
import java.util.Set;
/**
-
-
车队类 -> 调度管理出租车
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
由于细节内容实在太多了,为了不影响文章的观赏性,只截出了一部分知识点大致的介绍一下,每个小节点里面都有更细化的内容!
小编准备了一份Java进阶学习路线图(Xmind)以及来年金三银四必备的一份《Java面试必备指南》
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
…(img-vwW1Hxz8-1712587614814)]
[外链图片转存中…(img-0i6oPdYo-1712587614815)]
[外链图片转存中…(img-2gMTCjNx-1712587614815)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
由于细节内容实在太多了,为了不影响文章的观赏性,只截出了一部分知识点大致的介绍一下,每个小节点里面都有更细化的内容!
[外链图片转存中…(img-TfVrSfjQ-1712587614815)]
小编准备了一份Java进阶学习路线图(Xmind)以及来年金三银四必备的一份《Java面试必备指南》
[外链图片转存中…(img-WHlA0gUe-1712587614816)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!