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

Java中各种死锁详细讲述及其解决方案(图文并茂,浅显易懂)1 简介在遇到线程安全问题的时候 我们会使用加锁机制来确保线程安全 但如果过度地使用加锁 则可能导致锁顺序死锁 Lock OrderingDead 或者有的场景我们使用线程池和信号量来限制资源的使用 但这些被限制的行为可能会导致资源死锁 ResourceDead 这是来自 Java 并发必读佳作 JavaConcurre 关于活跃性危险中的描述 我们知道 Java 应用程序不像数据库服务器 能够检测一组事务中死锁的发生 进而选择一个事务去执行 在 Java 程

1、简介

2、死锁产生的条件

死锁的产生有四个必要的条件

  1. 互斥使用,即当资源被一个线程占用时,别的线程不能使用
  2. 不可抢占,资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放
  3. 请求和保持,当资源请求者在请求其他资源的同时保持对原因资源的占有
  4. 循环等待,多个线程存在环路的锁依赖关系而永远等待下去,例如T1占有T2的资源,T2占有T3的资源,T3占有T1的资源,这种情况可能会形成一个等待环路

对于死锁产生的四个条件只要能破坏其中一条即可让死锁消失,但是条件一是基础,不能被破坏。

3、各种死锁的介绍

3.1 锁顺序死锁

LeftRightDeadLock示例代码:

package com.liziba.dl; / * 

* 顺序死锁 *

* * @Author: Liziba */
public class LeftRightDeadLock { private final Object right = new Object(); private final Object left = new Object(); / * 加锁顺序从left -> right */ public void leftToRight() { synchronized (left) { synchronized (right) { System.out.println(Thread.currentThread().getName() + " left -> right lock."); } } } / * 加锁顺序right -> left */ public void rightToLeft() { synchronized (right) { synchronized (left) { System.out.println(Thread.currentThread().getName() + " right -> left lock."); } } } }

测试代码,通过创建多个线程,并发执行上面的LeftRightDeadLock

public static void main(String[] args) { 
    LeftRightDeadLock lrDeadLock = new LeftRightDeadLock(); for (int i = 0; i < 10; i++) { 
    new Thread(() -> { 
    // 为了更好的演示死锁,将两个方法的调用放置到同一个线程中执行 lrDeadLock.leftToRight(); lrDeadLock.rightToLeft(); }, "ThreadA-"+i).start(); } } 

在这里插入图片描述
jstack查看进程中的线程信息,线程信息比较多,我把重要的复制出来,如下的图中能很明显的看到产生了死锁。

在这里插入图片描述
这里省略了很多线程当前状态信息

在这里插入图片描述

3.2 动态锁顺序死锁

3.2.1 动态锁顺序死锁的产生与示例
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("", new BigDecimal(10000)); Account accountB = new Account("", 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(); } } 

3.2.2 动态锁顺序死锁的解决
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; / * 

* 车队类 -> 调度管理出租车 *

* * @Author: Liziba */
public class Fleet { / 车队中所有出租车 */ private final Set<Taxi> taxis; / 车队中目前空闲的出租车 */ private final Set<Taxi> available; public Fleet(Set<Taxi> taxis) { this.taxis = this.available = taxis; / * 出租车到达目的地后调用该方法,向车队发出当前出租车空闲信息 * * @param taxi */ public synchronized void free(Taxi taxi) { available.add(taxi); } / * 获取所有出租车在不同时刻的地址快照 * @return */ public synchronized Image getImage() { Image image = new Image(); for (Taxi taxi : taxis) { image.drawMarker(taxi); } return image; } }

Image(车辆地址信息快照类)示例代码:

package com.liziba.dl; import java.util.HashMap; import java.util.Map; / * 

* 获取所有出租车在某一时刻的位置快照 *

* * @Author: Liziba */
public class Image { Map<String, Coordinate> locationSnapshot = new HashMap<>(); public void drawMarker(Taxi taxi) { locationSnapshot.put(taxi.getId(), taxi.getLocation()); } }

在上述代码中,看不到一个方法中有对多个资源直接加锁,但仔细分析却能发现在方法的调用之间是存在对多个资源“隐式”加锁的,比如Taxi中的setLocation(Coordinate location)与Fleet中的Image getImage()。

  • setLocation(Coordinate location)方法需要获取当前出租车Taxi对象的锁以及出租车所属车队Fleet的锁
  • getImage()方法需要获取当前车队Fleet的锁,以及在遍历出租车获取其地址信息时需要获取每个出租车Taxi对象的锁
3.3.2 协作对象之间的死锁解决

Taxi -> TaxiOptimize(优化出租车类):

package com.liziba.dl; import java.util.Objects; / * 

* 出租车类优化 *

* * @Author: Liziba */
public class TaxiOptimize { // 省略相同的属性和函数 / * 优化内容 * setLocation(Coordinate location)方法不在加锁 * 将同步范围(锁住的代码)缩小 * this的锁与fleet顺序获取 ,锁内没有嵌套,不会死锁 * * @param location */ public void setLocation(Coordinate location) { this.location = location; boolean release = false; synchronized (this) { if (location.equals(destination)) { release = true; } } if (release) { fleet.free(this); } } }

Fleet -> FleetOptimize(优化出租车车队类):

package com.liziba.dl; import java.util.HashSet; import java.util.Set; / * 

* 出租车车队类优化 *

* * @Author: Liziba */
public class FleetOptimize { // 省略相同的属性和函数 / * 优化内容 * getImage()不再加锁 * 将同步范围(锁住的代码)缩小 * this(出租车车队对象)与drawMarker()方法中获取taxi对象的锁不再嵌套不会死锁 * * @return */ public Image getImage() { Set<TaxiOptimize> copy ; synchronized (this) { copy = new HashSet<TaxiOptimize>(taxis); } Image image = new Image(); for (TaxiOptimize taxi : copy) { image.drawMarker(taxi); } return image; } }

3.4 资源死锁

3.4.1 数据库连接池资源死锁
3.4.2 线程饥饿死锁

如下通过Executors.newSingleThreadExecutor()构建一个只有一个线程的线程池,提交的主任务会再次提交两个任务到这个线程池中去执行,在主任务中等待两个子任务的结果,而子任务又必须等到主任务执行结束后才能执行,这种情况就会产生线程饥饿死锁。

package com.lizba.currency.deadlock; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; / * 

* 单线程Executor中任务发送死锁 *

* * @Author: Liziba * @Date: 2021/7/1 21:25 */
public class ThreadDeadLock { / 单个线程的线程池 */ static ExecutorService executorService = Executors.newSingleThreadExecutor(); public static class Task1 implements Callable<String> { @Override public String call() throws Exception { Future<String> first = executorService.submit(new Task2()); Future<String> second = executorService.submit(new Task2()); // 当前任务等待子任务的结果,但是两个子任务在等待主任务完成,导致死锁 return first.get() + second.get(); } } public static class Task2 implements Callable<String> { @Override public String call() throws Exception { return "Hello Java"; } } / * 测试 * @param args */ public static void main(String[] args) { executorService.submit(new Task1()); } }

4、死锁的避免和诊断

关于死锁的避免主要是这几个方面:

  1. 尽可能使用无锁编程,使用开放调用的编码设计
  2. 尽可能的缩小锁的范围,防止锁住的资源过多引发阻塞和饥饿
  3. 如果加锁的时候需要获取多个锁则应该正确的设计锁的顺序
  4. 使用定时锁,比如Lock中的tryLock()

关于死锁的诊断主要是这几个方面:

  1. 找出代码什么地方会使用多个锁,对这些代码实例进行全局分析
  2. 通过线程转储(Thread Dump)信息来分析死锁

5、死锁以外的其他活跃性危险

除了死锁以外,并发的程序中可能还会存在以下几种风险

5.1 饥饿

  1. 不随意改变线程的优先级,尽量使得线程的优先级一致(这个在大部分场景都是适用的)
  2. 任务的执行尽量保持随机性或者公平性(性能考虑优先)

5.2 响应时间长

  1. 异步执行
  2. 避免代码中锁住的资源过大或者是CPU密集型的资源(尽量优化)
  3. 提升硬件设备
  4. 合理的设计线程执行的优先级

5.3 活锁

  1. 增加重试的随机性
  2. 增大重试间隔时间
  3. 设置最大重试次数




版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/229578.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月16日 下午4:23
下一篇 2026年3月16日 下午4:23


相关推荐

  • 【小白币看】数字货币火了这么久你还不知道如何挖矿?

    【小白币看】数字货币火了这么久你还不知道如何挖矿?​1什么是虚拟货币挖矿?挖矿指的是通过电脑CPU(内存)、GPU(显卡)或专业的矿机参与网络记账形成工作量证明POW,根据工作量证明的占比获得相应的奖励。目前,通过挖矿可以获得的虚拟货币常有:BTC(比特币)、ETH(以太坊)、ETC(以太经典)、ZEC(zcash零币)、SC(SIACOIN)等,上述过程简称挖矿。如果上述的文字解释有点复杂,可以看看下面小编图举的例子。2虚拟货币挖矿演变的三大过…

    2022年6月4日
    44
  • idea 2021 mac 激活码(JetBrains全家桶)

    (idea 2021 mac 激活码)2021最新分享一个能用的的激活码出来,希望能帮到需要激活的朋友。目前这个是能用的,但是用的人多了之后也会失效,会不定时更新的,大家持续关注此网站~IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/100143.html…

    2022年3月21日
    115
  • MSAgent 详细解说(上)「建议收藏」

    MSAgent 详细解说(上)「建议收藏」转:http://www.blueidea.com/tech/web/2004/1643.asp 作者windy_sk 邮箱:windy_sk@126.com下面是原文:本文完全原创,所有参考都是msdn.microsoft.com/library/en-us/msagent/agentstartpage_7gdh.asp引子:本来两年前就打算写了,结果拖了这么长时间,近日看到有朋友问及…

    2022年4月19日
    53
  • DeepSeek 接入 Word 完整教程:开发者高效办公指南

    DeepSeek 接入 Word 完整教程:开发者高效办公指南

    2026年3月16日
    3
  • java snmp walk,snmpwalk命令常用方法总结(转)[通俗易懂]

    java snmp walk,snmpwalk命令常用方法总结(转)[通俗易懂]snmpwalk命令则是测试系统各种信息最有效的方法,常用的方法如下:1、snmpwalk-cpublic-v1-mALL10.0.1.52.1.3.6.1.2.1.25.1得到取得windows端的系统进程用户数等2、snmpwalk-cpublic-v1-mALL10.0.1.52.1.3.6.1.2.1.25.2.2取得系统总内存3、snmpwal…

    2022年6月17日
    377
  • ewebeditor漏洞利用总结

    ewebeditor漏洞利用总结先从最基本的记录起!通常入侵ewebeditor编辑器的步骤如下:1、首先访问默认管理页看是否存在。       默认管理页地址2.80以前为ewebeditor/admin_login.asp以后版本为admin/login.asp(各种语言的大家自己改后缀,本文就以asp来进行说明,下面不再细说了!)2、默认管理帐号密码!       默认管理页存在!我们就用帐号密码

    2022年7月14日
    61

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号