协作对象死锁及其解决方案
协作对象死锁及其解决方案
1.前言
在遇到转账等的需要保证线程安全的情况时,我们通常会使用加锁的方式来保证线程安全,但如果无法合理的使用锁,很可能导致死锁。或者有时我们使用线程池来进行资源的使用,如调用数据库,但无法合理使用锁也可能导致死锁。
Java程序无法自动检测死锁并预防,在Java程序中如果遇到死锁将会是一个非常严重的问题,轻则导致线程阻塞,程序响应时间变长,系统吞吐量变小;重则导致系统部分功能失去响应能力无法使用。因此我们应当预防和规避这些问题。
如果对Synchronized关键字的基本使用方法不是很清楚的话可以看一下这篇文章:
Java中Synchronized关键字的基本使用方法
2.死锁说明
死锁是指两个或多个线程在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
死锁产生的四个必要条件:
1)互斥条件:资源是独占且排他的,即任意时刻一个资源只能给一个线程使用。其他线程若申请一个资源,而该资源被另一线程占用时,则申请者只能等待,直到资源被占用者释放。
2)不可剥夺条件:线程所获得的资源在未使用完毕之前,不会被其他线程强行剥夺,而只能由获得该资源的线程进行释放。
3)请求和保持条件:线程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
4)循环等待条件:在发生死锁时必然存在一个线程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个线程等待环路,环路中每一个线程所占有的资源同时被另一个申请。
协作对象死锁
死锁的产生往往不像顺序死锁那样明显(关于顺序死锁可以看一下这本篇文章:顺序死锁及其解决方案),就算其存在死锁风险往往也只会在高并发的场景下才可能暴露出来(这并不意味着应用没有高并发就不用考虑死锁问题了,作为开发者,你无法预测用户的操作)。
接下来介绍一种隐藏的比较深的死锁,这种死锁往往产生于多个协作对象的函数调用不透明。
Coordinate:坐标类,记录出租车的经度和纬度。
Fleet: 出租车车队类,车队类包含两个集合:车队中所有车辆信息taxis和车队中当前空闲的出租车信息available,此外还提供获取车队中所有出租车当前地址信息快照的方法getImage()
Taxi:出租车类,出租车属于某个出租车车队Fleet,此外还包含当前坐标location和目的地坐标destination,出租车在更新目的地信息的时候会判断当前坐标与目的地坐标是否相等,相等则会通知所属车队车辆空闲,可以接收下一个目的地
Image: 车辆地址信息快照类,用于获取出租车的地址信息
/*** 坐标类*/
public class Coordinate {/*** 经度*/private Double longitude;/*** 纬度*/private Double latitude;public Coordinate(Double longitude, Double latitude) {this.longitude = longitude;this.latitude = latitude;}public Double getLongitude() {return longitude;}public void setLongitude(Double longitude) {this.longitude = longitude;}public Double getLatitude() {return latitude;}public void setLatitude(Double latitude) {this.latitude = latitude;}@Overridepublic String toString() {return "Coordinate{" +"longitude=" + longitude +", latitude=" + latitude +'}';}}
/*** 车队类 -> 调度管理出租车*/
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) {System.out.println("出租车到站了");available.add(taxi);}/*** 获取所有出租车在不同时刻的地址快照*需要获取当前车队Fleet的锁,以及在遍历出租车获取其地址信息时需要获取每个出租车Taxi对象的锁* @return*/public synchronized Image getImage() {Image image = new Image();for (Taxi taxi : taxis) {image.drawMarker(taxi);}return image;}}
/*** 出租车类*/
public class Taxi {/*** 出租车唯一标志*/private String id;/*** 当前坐标*/private Coordinate location;/*** 目的地坐标*/private Coordinate destination;/*** 所属车队*/private final Fleet fleet;/*** 获取当前地址信息** @return*/public synchronized Coordinate getLocation() {return location;}/*** 更新当前地址信息* 如果当前地址与目的地地址一致,则表名到达目的地需要通知车队,当前出租车空闲可用前往下一个目的地*需要获取当前出租车Taxi对象的锁以及出租车所属车队Fleet的锁* @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;}@Overridepublic 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);}@Overridepublic int hashCode() {return Objects.hash(location, destination);}
}
/*** 获取所有出租车在某一时刻的位置快照*/
public class Image {Map<String, Coordinate> locationSnapshot = new HashMap<>();public void drawMarker(Taxi taxi) {locationSnapshot.put(taxi.getId(), taxi.getLocation());System.out.println("出租车当前位置为:" + taxi.getLocation().toString());}}
public class TaxiDeadLock {public static void main(String[] args) {HashSet<Taxi> taxiList = new HashSet<Taxi>();Fleet fleet = new Fleet(taxiList);Taxi taxi1 = new Taxi(fleet);taxiList.add(taxi1);Coordinate coordinate = new Coordinate(1.1, 2.2);taxi1.setDestination(coordinate);taxi1.setLocation(new Coordinate(1.0, 2.0));for (int i = 0; i < 50; i++) {new Thread(() -> {fleet.getImage();}, "车队").start();new Thread(() -> {taxi1.setLocation(coordinate);}, "出租车").start();}}}


从图片上我们可以看出,出租车和车队互相锁住了对方所需要的资源,并且请求对方所占有的资源,进程进入了死锁状态。
让我们从代码层面上看:
车队调用getImage()方法获取所有出租车在不同时刻的地址快照时,会先锁住当前车队,之后遍历出租车集合获取每辆出租车地址信息,通过drawMarker(Taxi taxi)方法获取出租车地址信息,这时我们会调用taxi.getLocation()方法,此方法会锁住当前出租车。
出租车调用setLocation(Coordinate location)方法更新当前地址信息时,会先锁住当前出租车,之后当前地址与目的地地址进行判断,如果一致,则调用free(Taxi taxi)方法通知车队当前出租车空闲,此方法会锁住出租车所属的车队。
以上代码并非显式的在一个方法中对多个资源进行加锁,而是隐式的在多个类中对对多个方法进行加锁,粗看之下似乎没什么问题,但是细细分析,就会发现存在很大的死锁隐患。
使用同步方法可能会发生较长时间的阻塞,这就可能给了死锁的机会。基于此类问题,我们通常要对需求进行分析,然后尝试采用缩小锁的范围或者不加锁等方式来解决。
下面让我们分析一下上文代码中的死锁:车队获取所有出租车在不同时刻的地址快照时,是一定要加锁的,因为这并不是一个原子性操作,不加锁就有可能发生车队中原本是5辆车,循环的时候突然多了或者少了车的情况。出租车更新当前地址信息时同理。所以这两处的锁,我们是不可以去掉的。那我们只可以尝试采用缩小锁的范围来防范:
/*** 车队类 -> 调度管理出租车*/
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) {System.out.println("出租车到站了");available.add(taxi);}/*** 优化内容* getImage()不再是同步方法* 将同步范围(锁住的代码)缩小* this(出租车车队对象)与drawMarker()方法中获取taxi对象的锁不再嵌套不会死锁** @return*/public Image getImage() {Set<Taxi> copy;synchronized (this) {copy = new HashSet<Taxi>(taxis);}Image image = new Image();for (Taxi taxi : copy) {image.drawMarker(taxi);}return image;}}
/*** 出租车类*/
public class Taxi {/*** 出租车唯一标志*/private String id;/*** 当前坐标*/private Coordinate location;/*** 目的地坐标*/private Coordinate destination;/*** 所属车队*/private final Fleet fleet;/*** 获取当前地址信息** @return*/public synchronized Coordinate getLocation() {return location;}/*** 优化内容* setLocation(Coordinate location)方法不在加锁* 将同步范围(锁住的代码)缩小* this的锁与fleet顺序获取 ,锁内没有嵌套,不会死锁* @param*/public void setLocation(Coordinate location) {this.location = location;boolean release = false;synchronized (this) {if (location.equals(destination)) {release = true;}}if (release) {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;}@Overridepublic 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);}@Overridepublic int hashCode() {return Objects.hash(location, destination);}
}

从上文代码中可以看出,我们优化了车队类的getImage()方法,将其同步方法改为了同步代码块,缩小了锁的范围:当调用getImage()方法时,我们创建一个车队拷贝,然后上锁,确保车队拷贝和当前车队一致,之后解锁,用车队拷贝去循环获取出租车的锁。之前会出现死锁是因为车队类既锁住了自己又去请求出租车的锁,现在请求出租车的时候没有锁住自己,破坏了死锁的四个触发条件之一,构不成死锁,出租车类的方法同理。
简单的说车队类和出租车类的方法我们只要修改一个就破坏了死锁的条件,无法构成死锁,但是我们还是需要对其不合理的代码进行优化。
由此我们可以得出结论:对于协作对象死锁,我们通常要对需求进行分析,然后尝试采用缩小锁的范围或者不加锁等方式来破坏死锁的四个必须条件中的一个或多个来解决。
相关文章:
协作对象死锁及其解决方案
协作对象死锁及其解决方案 1.前言 在遇到转账等的需要保证线程安全的情况时,我们通常会使用加锁的方式来保证线程安全,但如果无法合理的使用锁,很可能导致死锁。或者有时我们使用线程池来进行资源的使用,如调用数据库࿰…...
良许也成为砖家啦~
大家好,我是良许。 没错,良许成为砖家啦,绝不是口嗨,有图有真相! 有人会说,咦,这明明是严宇啊,跟你良许有啥关系? 额。。老读者应该知道良许的来历—— 鄙人真名严宇&a…...
Java中的编程细节
前言: 学习过程中有不少时候遇到一些看似简单,做起来事倍功半的问题。我也想自己是个聪明人,学东西一听就懂,一学就会,马上就能灵活应用。但这种事不能强求,要么自己要看个十遍二十遍最后理清逻辑…...
Yolov8从pytorch到caffe (一) 环境搭建
Yolov8从pytorch到caffe (一) 环境搭建 1. 创建虚拟环境2. 安装pytorch与v8相关库3. 测试安装是否成功4. 测试推理图像在windows上配置YOLOv8的环境,训练自己的数据集并转换到caffemodel1. 创建虚拟环境 利用conda创建虚拟环境 conda create -n yolo python=3.8 -y 并进入ac…...
2023年CDGA考试-第16章-数据管理组织与角色期望(含答案)
2023年CDGA考试-第16章-数据管理组织与角色期望(含答案) 单选题 1.在定义任何新组织或尝试改进现有组织之前了解当前组织的哪些方面非常重要? A.企业文化、运营模式和人员 B.业务战略、技术战略、数据战略 C.工具、方法和流程 D.事业环境因素、组织过程资产,行动路线图 …...
Stream——集合数据按照某一字段排序
文章目录前言假设业务场景排序前的准备正序排序1、数据集合的判空 Optional.isPresent()2、使用sort排序3、将排序后的数据流转换为list你以为这样就完了?倒序排序前言 之前,针对Stream链式编程中的几个方法做了大致的说明。详情可以参考: J…...
ubuntu:20.04编译arrow
1)拉取代码 git clone https://github.com/apache/arrow.git 2)切换分支 git checkout apache-arrow-11.0.0 3)拉入测试数据并设置环境变量 pushd arrow git submodule update --init export PARQUET_TEST_DATA"${PWD}/cpp/submodules/parquet-testing/da…...
2023如果纯做业务测试的话,在测试行业有出路吗?
直接抛出我的结论:手工做业务类测试,没有前途。 个人建议赶紧从业务测试跳出来,立即学习代码,走自动化测试方向。目前趋势,业务测试需要用自动化做。 为了让大家能够信服我的观点,本文将从以下方面进行阐…...
golang grpc ssl
无CA场景 在不考虑CA的场景下呢,client有client.key和client.crt,server有server.key和server.crt,生成方式可以如下: $ openssl genrsa -out server.key 2048 $ openssl req -new -x509 -days 3650 \-subj "/CGB/LChina/Og…...
华为服务器驱动下载及安装
1.服务器技术支持网站 https://support.xfusion.com/support/#/zh/home 2.选择软件下载 3.选择服务器型号 4.选择驱动 5.根据需求选择驱动 例如红帽7.4系统 6.安装驱动 自动安装驱动步骤: 1)使用BMC虚拟光驱挂载onboard_driver_xxx.iso: 2)mount /dev/sr0 /mnt …...
【Shell】常用命令合集
常用命令: 文件和目录: cd /home 进入 ‘/home’ 目录 cd … 返回上一级目录 cd …/… 返回上两级目录 cd - 返回上次所在目录 cp file1 file2 将file1复制为file2 cp -a dir1 dir2 复制一个目录 cp -a /tmp/dir1 . 复制一个目录到当前工作目录(.代表当前目录…...
15- 答题卡识别及分数判定项目 (OpenCV系列) (项目十五)
项目要点 图片读取 : img cv2.imread(./images/test_01.png)灰度图: gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)高斯模糊: blurred cv2.GaussianBlur(gray, (5, 5), 0) # 去噪点边缘检测: edged cv2.Canny(blurred, 75, 200)检测轮廓: cnts cv2.findContours(e…...
LeetCode 热题 C++ 146. LRU 缓存
力扣146 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否…...
Java线程池使用与原理解析(线程池优点、使用方法、参数含义及线程池运转机制)
为什么要使用线程池? JDK1.5后JUC包添加了线程池相关接口,在Java诞生之初并没有线程池这个概念。刚开始Java程序都是自行创建线程去处理任务。随着应用使用的线程越来越多,JDK开发者们发现有必要使用一个统一的类来管理这些线程,…...
mybatis入门配置
mybatis mybatis是一款持久层框架,用于简化JDBC开发 持久层:负责将数据保存到数据库的那一层代码JavaEE的三层架构:表现层、业务层、持久层、,就相当与mvc设计模式过程中的Controller、service、dao 1.创建一个maven模块&#…...
黑客入门(超级详细版)
据我了解,“黑客”大体上应该分为“正”、“邪”两类,正派黑客依靠自己掌握的知识帮助系统管理员找出系统中的漏洞并加以完善,而邪派黑客则是通过各种黑客技能对系统进行攻击、入侵或者做其他一些有害于网络的事情,因为邪派黑客所…...
Java多线程(三)---synchronized、Lock和volatile
Java内存模型(非JVM)Java内存模型(Java Memory Model简称JMM),是一种共享内存模型,是多线程的东西,并不是JVM(Java Virtual Machine(Java虚拟机)的缩写),这是俩玩意儿!&a…...
JVM-Java内存区域
运行时数据区:1、程序计数器:当前线程所执行的字节码指令的行号指示器。在Java虚拟机的概念模型里,字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环…...
毕业季,毕业论文查重,paper系列五个免费查重网站推荐
推荐五个常用的免费查重网站 注意: (1)这些网站基本上都可以通过关注公众号或者转发等来获取免费查重机会,但有些也会有字数限制。每个网站的数据库可能不同,所以建议大家多换几个平台查一查,反正是免费的…...
破解票房之谜:为何高票房电影绕不过“猫眼们”?
如此火爆的春节档很多,如此毁誉参半的春节档鲜有。2023开年,集齐张艺谋、沈腾的《满江红》,以及有票房前作打底的《流浪地球2》接连两部春节档电影票房进入前十,为有些颓靡的中国电影市场注入了一针“强心剂”。与票房同样热闹起来…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...
多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...
遍历 Map 类型集合的方法汇总
1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...
安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
连锁超市冷库节能解决方案:如何实现超市降本增效
在连锁超市冷库运营中,高能耗、设备损耗快、人工管理低效等问题长期困扰企业。御控冷库节能解决方案通过智能控制化霜、按需化霜、实时监控、故障诊断、自动预警、远程控制开关六大核心技术,实现年省电费15%-60%,且不改动原有装备、安装快捷、…...
系统设计 --- MongoDB亿级数据查询优化策略
系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log,共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题,不能使用ELK只能使用…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...
苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...
