Java中各种死锁详细讲述及其解决方案(图文并茂,浅显易懂

产生这种情况的原因,是不同的线程通过不同顺序去获取相同的锁;比如线程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 协作对象死锁的产生与示例

死锁的产生往往没有上述两种死锁产生的那么明显,就算其存在死锁风险也只有在高并发的场景下才会暴露出来(这并不意味着没得高并发的应用就不用考虑死锁问题了啊,弟兄们!)。如下介绍一种隐藏的比较深的死锁,这种死锁产生在多个协作对象的函数调用不透明。

如下以出租车为例介绍协作对象之间死锁的产生,其主要涉及到以下几个类(省略了很多代码,自行脑补哈!):

  1. Coordinate -> 坐标类,出租车经纬度信息类

  2. Taxi -> 出租车类,出租车所属于某个出租车车队Fleet,此外包含当前坐标location和目的地坐标destination,出租车在更新目的地信息的时候会判断当前坐标与目的地坐标是否相等,相等则会通知所属车队车辆空闲,可以接收下一个目的地

  3. Fleet -> 出租车车队类,出租车类包含两个集合taxis和available,分别用来保存车队中所有车辆信息和车队中当前空闲的出租车信息,此外提供获取车队中所有出租车当前地址信息的快照方法getImage()

  4. 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开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

看完上述知识点如果你深感Java基础不够扎实,或者刷题刷的不够、知识不全面

小编专门为你量身定制了一套<Java一线大厂高岗面试题解析合集:JAVA基础-中级-高级面试+SSM框架+分布式+性能调优+微服务+并发编程+网络+设计模式+数据结构与算法>

image

针对知识面不够,也莫慌!还有一整套的<Java核心进阶手册>,可以瞬间查漏补缺

image

全都是一丢一丢的收集整理纯手打出来的

更有纯手绘的各大知识体系大纲,可供梳理:Java筑基、MySQL、Redis、并发编程、Spring、分布式高性能架构知识、微服务架构知识、开源框架知识点等等的xmind手绘图~

image

image
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

看完上述知识点如果你深感Java基础不够扎实,或者刷题刷的不够、知识不全面

小编专门为你量身定制了一套<Java一线大厂高岗面试题解析合集:JAVA基础-中级-高级面试+SSM框架+分布式+性能调优+微服务+并发编程+网络+设计模式+数据结构与算法>

[外链图片转存中…(img-14XYN1Tp-1711982611589)]

针对知识面不够,也莫慌!还有一整套的<Java核心进阶手册>,可以瞬间查漏补缺

[外链图片转存中…(img-6fACrce1-1711982611589)]

全都是一丢一丢的收集整理纯手打出来的

更有纯手绘的各大知识体系大纲,可供梳理:Java筑基、MySQL、Redis、并发编程、Spring、分布式高性能架构知识、微服务架构知识、开源框架知识点等等的xmind手绘图~

[外链图片转存中…(img-ZaIEfjw1-1711982611590)]

[外链图片转存中…(img-dVBszl3N-1711982611590)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值