备案 控制台
开发者社区 开发与运维 文章 正文

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

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

1、简介

在遇到线程安全问题的时候,我们会使用加锁机制来确保线程安全,但如果过度地使用加锁,则可能导致锁顺序死锁(Lock-Ordering Deadlock)。或者有的场景我们使用线程池和信号量来限制资源的使用,但这些被限制的行为可能会导致资源死锁(Resource DeadLock)。这是来自Java并发必读佳作 Java Concurrency in Practice 关于活跃性危险中的描述。

我们知道Java应用程序不像数据库服务器,能够检测一组事务中死锁的发生,进而选择一个事务去执行;在Java程序中如果遇到死锁将会是一个非常严重的问题,它轻则导致程序响应时间变长,系统吞吐量变小;重则导致应用中的某一个功能直接失去响应能力无法提供服务,这些后果都是不堪设想的。因此我们应该及时发现和规避这些问题。


2、死锁产生的条件

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


互斥使用,即当资源被一个线程占用时,别的线程不能使用

不可抢占,资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放

请求和保持,当资源请求者在请求其他资源的同时保持对原因资源的占有

循环等待,多个线程存在环路的锁依赖关系而永远等待下去,例如T1占有T2的资源,T2占有T3的资源,T3占有T1的资源,这种情况可能会形成一个等待环路

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


3、各种死锁的介绍

3.1 锁顺序死锁

先举一个顺序死锁的例子。

构建一个LeftRightDeadLock类,这个类中有两个共享资源right,left我们通过对这两个共享资源加锁的方式来控制程序的执行流程,但是这个示例在高并发的场景下存在顺序死锁的风险。

如下示意图存在死锁风险

image.pngimage.pngimage.png产生这种情况的原因,是不同的线程通过不同顺序去获取相同的锁;比如线程1获取锁的顺序是left -> right,而线程2获取锁的顺序是right -> left,在某种情况下会发生死锁。拿上面的案例分析,我们通过Java自带的jps和jstack工具查看java进程ID和线程相关信息。

jps查看LeftRightDeadLock的进程id为17968

image.pngimage.png解决顺序死锁的办法其实就是保证所有线程以相同的顺序获取锁就行。



3.2 动态锁顺序死锁

3.2.1 动态锁顺序死锁的产生与示例

动态锁顺序死锁与上面的锁顺序死锁其实最本质的区别,就在于动态锁顺序死锁锁住的资源无法确定或者会发生改变。

比如说银行转账业务中,账户A向账户B转账,账户B也可以向账户A转账,这种情况下如果加锁的方式不正确就会发生死锁,比如如下代码:

定义简单的账户类Account

image.pngimage.png上面这个类看似规定了锁的顺序由accountFrom到accountTo不会产生死锁,但是这个accountFrom和accountTo是由调用方来传入的,当A向B转账时accountFrom = A,accountTo = B;当B向A转账时accountFrom = B,accountTo = A;假设两者在同一时刻给对方发起转账,则仍然存在3.1中锁顺序死锁问题。比如如下测试:

image.pngimage.png

3.2.2 动态锁顺序死锁的解决

解决动态锁顺序死锁的办法,就是通过一定的手段来严格控制加锁的顺序。比如通过对象中某一个唯一的属性值比如id;或者也可以通过对象的散列值+hash冲突解决来控制加锁的顺序。

我们通过对象的散列值+hash冲突解决的方式来优化上面的代码:

package com.liziba.dl;
import java.math.BigDecimal;
/**
 * <p>
 * 转账类优化 -> 通过hash算法
 * </p>
 *
 * @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());
        }
    }
}

image.png在上面两种死锁的产生原因都是因为两个线程以不同的顺序获取相同的所导致的,而解决的办法都是通过一定的规范来严格控制加锁的顺序,这样就能正确的规避死锁的风险。


3.3 协作对象之间的死锁

3.3.1 协作对象死锁的产生与示例

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

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


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

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

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

Image -> 车辆地址信息快照类,用于获取出租车的地址信息

Coordinate(坐标类) 代码示例:

image.pngTaxi(出租车类)代码示例;

package com.liziba.dl;
import java.util.Objects;
/**
 * <p>
 *      出租车类
 * </p>
 *
 * @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;
/**
 * <p>
 *      车队类 -> 调度管理出租车
 * </p>
 *
 * @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.png在上述代码中,看不到一个方法中有对多个资源直接加锁,但仔细分析却能发现在方法的调用之间是存在对多个资源“隐式”加锁的,比如Taxi中的setLocation(Coordinate location)与Fleet中的Image getImage()。


setLocation(Coordinate location)方法需要获取当前出租车Taxi对象的锁以及出租车所属车队Fleet的锁

getImage()方法需要获取当前车队Fleet的锁,以及在遍历出租车获取其地址信息时需要获取每个出租车Taxi对象的锁

如上所示的这两种情况无法避免同时执行的情况,因此存在死锁的可能性,其执行流程如下:

image.png3.3.2 协作对象之间的死锁解决

Taxi中的setLocation(Coordinate location)方法与getImage()方法中包含其他方法的调用,方法的调用应该是透明的也就是说,调用方无需知道方法内部的执行逻辑,这是正确的。但是方法中调用的其他方法可能是同步方法或者方法中会发生较长时间的阻塞,这会导致死锁或者线程长时间等待等问题。基于此类问题,可以采用缩小同步代码的访问(锁尽可能少的代码)和开放调用(不加锁)来解决(Open Call)。

上述代码我们基于上面提的两种方式来优化:


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

image.pngimage.png上述的代码虽然在同步语义上有一定的改变,但是符合业务场景的需求。具体在开发中怎么去抉择锁的范围和加锁的顺序,需要各位开发大佬仔细斟酌,毕竟加锁的代码就那么点,用的好名垂千古,用不好遗臭万年,哈哈哈哈。


3.4 资源死锁

3.4.1 数据库连接池资源死锁

上面发生的死锁都是两个线程相互持有对方需要获取的锁资源又不释放本身持有的锁;而资源死锁与上面的案例有些相似,只是这里相互持有对方需要的资源(比如数据库连接池中的数据库连接)。现在假设有两个数据库连接池分别用来访问数据库A和数据库B,这时有多个任务需要同时访问数据库A和数据库B,他们都需要从数据库连接池中获取连接才能访问对应的数据库。做个极端的假设,数据库连接池A只有一个连接,数据库连接池B也只有一个连接(这只是为了更好的理解资源死锁的产生!),那么此时可能会出现下面所示的情况:

image.png如上的这种情况在数据库连接池中连接数量较高的时候发生的情况是十分少的,但也并不是完全没有可能。


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;
/**
 * <p>
 *      单线程Executor中任务发送死锁
 * </p>
 *
 * @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、死锁的避免和诊断

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


尽可能使用无锁编程,使用开放调用的编码设计

尽可能的缩小锁的范围,防止锁住的资源过多引发阻塞和饥饿

如果加锁的时候需要获取多个锁则应该正确的设计锁的顺序

使用定时锁,比如Lock中的tryLock()

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


找出代码什么地方会使用多个锁,对这些代码实例进行全局分析

通过线程转储(Thread Dump)信息来分析死锁


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

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


5.1 饥饿

线程饥饿在上面的线程池案例中也提到过,它指的是当前线程无法获取到CPU的执行周期(一直被其他线程占用执行),类似发生的还有在ReetranLock中的非公平锁的实现也可能会出现线程饥饿的问题。

关于线程饥饿的解决办法:


不随意改变线程的优先级,尽量使得线程的优先级一致(这个在大部分场景都是适用的)

任务的执行尽量保持随机性或者公平性(性能考虑优先)

5.2 响应时间长

响应时间长指的是某个线程执行的任务占有较长的CPU执行时间,会导致后续的操作阻塞,导致程序失去响应。比如说浏览某个网页,向服务端发起的某个请求中包含运行时间较长的任务,此时前端程序将会失去响应,使得用户体验极差。

关于响应时间长的解决办法:


异步执行

避免代码中锁住的资源过大或者是CPU密集型的资源(尽量优化)

提升硬件设备

合理的设计线程执行的优先级

5.3 活锁

活锁指的是线程不阻塞,会持续保持运行,但是这里的运行时重复的执行同一个任务。比如消息发送用队列来存储需要发送的消息,某条消息由于某些原因不能发送成功并且没有被丢弃或者做其他处理,而是直接回到队列的头部重新执行,这会导致这条消息一直循环不断的执行下去。

关于活锁的解决办法:


增加重试的随机性

增大重试间隔时间

设置最大重试次数





李子捌
目录
相关文章
java开发-郭老师
|
19天前
|
Java
Java 字符串分割split空字符串丢失解决方案
Java 字符串分割split空字符串丢失解决方案
java开发-郭老师
15 0
人不走空
|
1月前
|
编解码 Java Apache
Java中文乱码浅析及解决方案
Java中文乱码浅析及解决方案
人不走空
50 0
尘尘尘尘
|
1月前
|
Java
Java中的异常链:从根源到解决方案
Java中的异常链:从根源到解决方案
尘尘尘尘
37 0
源码星辰
|
1月前
|
搜索推荐 前端开发 Java
Java医院绩效考核系统解决方案源码
作为医院用综合绩效核算系统,系统需要和his系统进行对接,按照设定周期,从his系统获取医院科室和医生、护士、其他人员工作量,对没有录入信息化系统的工作量,绩效考核系统设有手工录入功能(可以批量导入),对获取的数据系统按照设定的公式进行汇算,且设置审核机制,可以退回修正,系统功能强大,完全模拟医院实际绩效核算过程,且每步核算都可以进行调整和参数设置,能适应医院多种绩效核算方式。
源码星辰
43 4
源码星辰
|
2月前
|
监控 安全 物联网
Java基于物联网技术的智慧工地解决方案源代码
应用先进的大数据、物联网、云计算等数字化技术,融合施工运营管理规范和技术标准,建构支撑施工和运营的一体化平台是投资、施工和运营单位能力建设的关键。应用企业架构、设计思维和软件工程方法,深入分析施工和运营技术特性与管理体系,研究开发基于大数据技术的智慧工地信息一体化平台,智慧工地管理平台是依托物联网、互联网建立的大数据管理平台,是一种全新的管理模式,能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。
源码星辰
85 2
请看我回答~
|
2月前
|
Java
Java并发编程中的死锁问题及解决方法
【2月更文挑战第9天】在Java并发编程中,死锁是一种常见但又令人头疼的问题。本文将深入探讨死锁产生的原因,以及针对不同情况所提供的解决方法,帮助读者更好地理解和应对死锁。
请看我回答~
36 2
技术混子
|
1月前
|
Java
Java并发编程中的死锁问题及解决方法
【2月更文挑战第11天】 在Java并发编程中,死锁是一个常见但又非常棘手的问题。本文将深入探讨死锁的概念、产生原因以及常见的解决方法,帮助读者更好地理解并发编程中的挑战,并提供实用的解决方案。
技术混子
39 6
天下无贼001
|
2月前
|
安全 Java 调度
Java中的并发编程挑战与解决方案
【2月更文挑战第5天】在日益复杂的软件开发环境中,Java作为一种广泛应用的编程语言,面临着越来越多的并发编程挑战。本文将探讨Java中常见的并发问题,并提供相应的解决方案,帮助开发人员更好地应对并发编程中的挑战。
天下无贼001
23 3
阿里云云123
|
2月前
|
缓存 前端开发 安全
前后端分离架构下Java Web开发的挑战与解决方案
前后端分离架构下Java Web开发的挑战与解决方案
阿里云云123
44 1
阿里云云123
|
2月前
|
运维 监控 Java
使用Java进行性能监控可能会遇到的问题以及解决方案
使用Java进行性能监控可能会遇到的问题以及解决方案
阿里云云123
31 0

热门文章

最新文章

  • 1
    Java中的多线程编程:概念、实现与性能优化
  • 2
    使用Redis进行Java缓存策略设计
  • 3
    Java语言开发的AI智慧导诊系统源码springboot+redis 3D互联网智导诊系统源码
  • 4
    基于 java + Springboot + vue +mysql 大学生实习管理系统(含源码)
  • 5
    Java 中文官方教程 2022 版(二十九)(2)
  • 6
    使用Java代码打印log日志
  • 7
    Java 中文官方教程 2022 版(二十八)(1)
  • 8
    Java 中文官方教程 2022 版(十四)(1)
  • 9
    Java 中文官方教程 2022 版(九)(2)
  • 10
    深入理解Java并发编程:线程安全与性能优化
  • 1
    Serverless 应用引擎操作报错合集之在Serverless 应用引擎中,FC3.0读取response body的时候出现错误提示"Caused by: java.io.IOException: closed"如何解决
    12
  • 2
    Java 测试和调试:提高代码质量的实用策略
    9
  • 3
    Java 编程风格与规范:提升代码质量与可维护性
    8
  • 4
    Java 设计模式:探索发布-订阅模式的原理与应用
    8
  • 5
    Java 设计模式:探索策略模式的概念和实战应用
    6
  • 6
    Java 异步编程:概念、优势与实战示例
    7
  • 7
    Java 模块化编程:概念、优势与实战指南
    8
  • 8
    【专栏】Java中的反射机制与应用实例
    12
  • 9
    【JAVA】HashMap扩容性能影响及优化策略
    13
  • 10
    滚雪球学Java(23):包机制
    15
  • 相关课程

    更多
  • Java面试疑难点解析 - 面试技巧及语言基础
  • Java面试疑难点解析 - Java Web开发
  • Java面试疑难点解析 - 系统架构及项目设计
  • Java编程入门
  • Java面向对象编程
  • Java高级编程
  • 相关电子书

    更多
  • Spring Cloud Alibaba - 重新定义 Java Cloud-Native
  • The Reactive Cloud Native Arch
  • JAVA开发手册1.5.0
  • 相关实验场景

    更多
  • 每个IT人都想学的“Web应用上云经典架构”实战
  • 语言入门-1:环境构建
  • 阿里云平台上进行Java程序的编译与运行
  • 使用Java面向对象编写网络通信程序应用
  • Python网络通信程序典型应用
  • Elasticsearch Java API Client 开发
  • 下一篇
    部署LAMP环境(Alibaba Cloud Linux 3)

    聚圣源摄影工作室起名起名声调裸婚时代全集古灵小说全集风水小说2009年日历安字起名大全男孩名字大全惠州区号掌中宝周易起名大师ok老板娘奥特之母怪兽侮辱视频乌鲁木齐起名哪里好京东商城会有假货吗学校起名字三个字的的品牌起名巴音郭楞精雕油泥沈蓓一宁少辰全文免费阅读21cake.com牛宝宝起名字女孩2021孔德起名男孩建筑劳务公司起什么店名好大杳蕉狼人欧美埃罗芒阿老师在线观看免费第一季经典散文欣赏奇门遁甲起名准确吗约炮是什么意思真相演员表狗的孩子起名宜用字男孩起什么名字淀粉肠小王子日销售额涨超10倍罗斯否认插足凯特王妃婚姻让美丽中国“从细节出发”清明节放假3天调休1天男孩疑遭霸凌 家长讨说法被踢出群国产伟哥去年销售近13亿网友建议重庆地铁不准乘客携带菜筐雅江山火三名扑火人员牺牲系谣言代拍被何赛飞拿着魔杖追着打月嫂回应掌掴婴儿是在赶虫子山西高速一大巴发生事故 已致13死高中生被打伤下体休学 邯郸通报李梦为奥运任务婉拒WNBA邀请19岁小伙救下5人后溺亡 多方发声王树国3次鞠躬告别西交大师生单亲妈妈陷入热恋 14岁儿子报警315晚会后胖东来又人满为患了倪萍分享减重40斤方法王楚钦登顶三项第一今日春分两大学生合买彩票中奖一人不认账张家界的山上“长”满了韩国人?周杰伦一审败诉网易房客欠租失踪 房东直发愁男子持台球杆殴打2名女店员被抓男子被猫抓伤后确诊“猫抓病”“重生之我在北大当嫡校长”槽头肉企业被曝光前生意红火男孩8年未见母亲被告知被遗忘恒大被罚41.75亿到底怎么缴网友洛杉矶偶遇贾玲杨倩无缘巴黎奥运张立群任西安交通大学校长黑马情侣提车了西双版纳热带植物园回应蜉蝣大爆发妈妈回应孩子在校撞护栏坠楼考生莫言也上北大硕士复试名单了韩国首次吊销离岗医生执照奥巴马现身唐宁街 黑色着装引猜测沈阳一轿车冲入人行道致3死2伤阿根廷将发行1万与2万面值的纸币外国人感慨凌晨的中国很安全男子被流浪猫绊倒 投喂者赔24万手机成瘾是影响睡眠质量重要因素春分“立蛋”成功率更高?胖东来员工每周单休无小长假“开封王婆”爆火:促成四五十对专家建议不必谈骨泥色变浙江一高校内汽车冲撞行人 多人受伤许家印被限制高消费

    聚圣源 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化