当前位置: 首页 > news >正文

什么是线程死锁?如何解决死锁问题

死锁,一组互相竞争的资源的线程之间相互等待,导致永久阻塞的现象。

如下图所示:
在这里插入图片描述
与死锁对应的,还有活锁,是指线程没有出现阻塞,但是无限循环。


有一个经典的银行转账例子如下:

我们有个账户类,其中有两个属性:账户名和余额。
它有两个方法:转入、转出。

public class Account {private String countName;private int balance;public Account(String countName, int balance) {this.countName = countName;this.balance = balance;}/*** 转出金额,更新转出方余额,金额减少* @param amount*/public void debit(int amount){this.balance -= amount;}/*** 存入金额,更新转入方余额,金额增多* @param amount*/public void credit(int amount){this.balance += amount;}public String getCountName() {return countName;}public void setCountName(String countName) {this.countName = countName;}public int getBalance() {return balance;}public void setBalance(int balance) {this.balance = balance;}
}

还有一个转账操作类,它有三个属性:转入账户、转出账户、转账金额。
它实现了Runnable接口,run方法中是转账逻辑代码。
我们使用 while(true) 让他不停的进行转账操作:如果账户余额大于转账金额,就让转出账号减少amount,转入账户增加amount。打印出线程名、金额转移方向、以及每个账户余额。
我们在main方法中进行测试,创建两个转账账户,使用两个线程操作这两个账户,让他们相互转账。

public class TransferAccount implements Runnable{private Account fromAccount;private Account toAccount;private int amount;public TransferAccount(Account fromAccount, Account toAccount, int amount) {this.fromAccount = fromAccount;this.toAccount = toAccount;this.amount = amount;}@Overridepublic void run() {while(true){synchronized (fromAccount){synchronized (toAccount){if(fromAccount.getBalance()>=amount) {fromAccount.debit(amount);toAccount.credit(amount);}System.out.println(Thread.currentThread().getName());System.out.println(fromAccount.getCountName()+"->"+toAccount.getCountName()+":"+amount );System.out.println(fromAccount.getCountName() +"账户余额:"+ fromAccount.getBalance());System.out.println(toAccount.getCountName() +"账户余额:"+ toAccount.getBalance());}}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {Account bigHead = new Account("冤大头",100000);Account smallKill = new Account("门小抠",200000);new Thread(new TransferAccount(bigHead,smallKill,10)).start();new Thread(new TransferAccount(smallKill,bigHead,20)).start();}}

执行main方法后的输出结果:

Thread-0
冤大头->门小抠:10
冤大头账户余额:99990
门小抠账户余额:200010
Thread-1
门小抠->冤大头:20
门小抠账户余额:199990
冤大头账户余额:100010
Thread-1
门小抠->冤大头:20
门小抠账户余额:199970
冤大头账户余额:100030
Thread-0
冤大头->门小抠:10
冤大头账户余额:100020
门小抠账户余额:199980
Thread-0
冤大头->门小抠:10
冤大头账户余额:100010
门小抠账户余额:199990
Thread-1
门小抠->冤大头:20
门小抠账户余额:199970
冤大头账户余额:100030
Thread-0
冤大头->门小抠:10
冤大头账户余额:100020
门小抠账户余额:199980
Thread-1
门小抠->冤大头:20
门小抠账户余额:199960
冤大头账户余额:100040
Thread-0
冤大头->门小抠:10
冤大头账户余额:100030
门小抠账户余额:199970
Thread-1
门小抠->冤大头:20
门小抠账户余额:199950
冤大头账户余额:100050
Thread-0
冤大头->门小抠:10
冤大头账户余额:100040
门小抠账户余额:199960
Thread-1
门小抠->冤大头:20
门小抠账户余额:199940
冤大头账户余额:100060

这时,我们发现进程还在运行,但是控台停止输出了,它停在那里了。
截图如下:
在这里插入图片描述

这时我们使用 jps 来查看一下java进程:

D:\open_source\MyBatis\MyThread\target\classes\demo>jps
23600
24676 Launcher
40244 Jps
23672 TransferAccount

然后输入 使用jstack来看详情:

D:\open_source\MyBatis\MyThread\target\classes\demo>jstack 23672
2023-02-26 18:37:57
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.351-b10 mixed mode):"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x000001b4fc785800 nid=0x6530 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Thread-1" #13 prio=5 os_prio=0 tid=0x000001b4fc77f800 nid=0x887c waiting for monitor entry [0x000000467adff000]java.lang.Thread.State: BLOCKED (on object monitor)at deadlock.TransferAccount.run(TransferAccount.java:20)- waiting to lock <0x000000076c5a4610> (a deadlock.Account)- locked <0x000000076c5a4658> (a deadlock.Account)at java.lang.Thread.run(Thread.java:750)"Thread-0" #12 prio=5 os_prio=0 tid=0x000001b4fc77c800 nid=0x8c80 waiting for monitor entry [0x000000467acff000]java.lang.Thread.State: BLOCKED (on object monitor)at deadlock.TransferAccount.run(TransferAccount.java:20)- waiting to lock <0x000000076c5a4658> (a deadlock.Account)- locked <0x000000076c5a4610> (a deadlock.Account)at java.lang.Thread.run(Thread.java:750)......Found one Java-level deadlock:
=============================
"Thread-1":waiting to lock monitor 0x000001b4fa208eb8 (object 0x000000076c5a4610, a deadlock.Account),which is held by "Thread-0"
"Thread-0":waiting to lock monitor 0x000001b4fa208e08 (object 0x000000076c5a4658, a deadlock.Account),which is held by "Thread-1"Java stack information for the threads listed above:
===================================================
"Thread-1":at deadlock.TransferAccount.run(TransferAccount.java:20)- waiting to lock <0x000000076c5a4610> (a deadlock.Account)- locked <0x000000076c5a4658> (a deadlock.Account)at java.lang.Thread.run(Thread.java:750)
"Thread-0":at deadlock.TransferAccount.run(TransferAccount.java:20)- waiting to lock <0x000000076c5a4658> (a deadlock.Account)- locked <0x000000076c5a4610> (a deadlock.Account)at java.lang.Thread.run(Thread.java:750)Found 1 deadlock.

我们可以看到,其中Thread-0和Thread-1都已经处于BLOCK状态,发生了死锁。


导致死锁发生,必须同时满足四个条件:互斥、占有且等待、不可抢占、循环等待。

  • 互斥:共享资源A和B只能被一个线程占用。
  • 占有且等待:线程Thread-1 已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X。
  • 不可抢占:其他线程不能强行抢占线程Thread-1 占有的资源。
  • 循环等待:线程Thread-1 等待线程Thread-2 占有的资源,线程Thread-2等待Thread-1 占有的资源。

如何解决?

如果要解决死锁问题,只需要破坏其中一个条件,使其不满足即可。
除了互斥(多线程的基础)之外,其他三个条件都可以考虑破坏。
实际中遇到死锁,只能重启,如果还是死锁,要定位问题点,然后修复代码(破坏掉死锁必须满足的条件之一)后重新发布。


我们来尝试破坏占有且等待的方式来破坏死锁:
新增加一个分配的类:Allocator,它有一个账户池。每个转账线程中要判断,账户池中是否已有此账户,如果没有可以转账,有的话就不转账。就可以避免共享资源同时存在于两个线程。

import java.util.ArrayList;
import java.util.List;public class Allocator {private List<Object> list = new ArrayList<>();synchronized boolean apply(Object fromAccount,Object toAccount){if(list.contains(fromAccount)||list.contains(toAccount)){return false;}list.add(fromAccount);list.add(toAccount);return true;}synchronized void free(Object fromAccount,Object toAccount){list.remove(fromAccount);list.remove(toAccount);}
}//相应的,TransAcount也要做修改:
public class TransferAccount implements Runnable {private Account fromAccount;private Account toAccount;private int amount;private Allocator allocator;public TransferAccount(Account fromAccount, Account toAccount, int amount, Allocator allocator) {this.fromAccount = fromAccount;this.toAccount = toAccount;this.amount = amount;this.allocator = allocator;}@Overridepublic void run() {while (true) {//判断账户是否已经分配过if (allocator.apply(fromAccount, toAccount)) {try {synchronized (fromAccount) {synchronized (toAccount) {if (fromAccount.getBalance() >= amount) {fromAccount.debit(amount);toAccount.credit(amount);}System.out.println(Thread.currentThread().getName());System.out.println(fromAccount.getCountName() + "->" + toAccount.getCountName() + ":" + amount);System.out.println(fromAccount.getCountName() + "账户余额:" + fromAccount.getBalance());System.out.println(toAccount.getCountName() + "账户余额:" + toAccount.getBalance());}}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}finally {allocator.free(fromAccount,toAccount);}}}}public static void main(String[] args) {Account bigHead = new Account("冤大头", 100000);Account smallKill = new Account("门小抠", 200000);Allocator allocator = new Allocator();new Thread(new TransferAccount(bigHead, smallKill, 10, allocator)).start();new Thread(new TransferAccount(smallKill, bigHead, 20, allocator)).start();}}

我们来避免第三个条件:使用Lock来替换掉 Synchronized。
Synchronized加锁后,要等到资源释放,而且锁是不可抢占的。
Lock中有个方法 tryLock,可以返回布尔值,如果返回fasle就不会进去。tryLock不会持续持有锁。

public class TransferAccount2 implements Runnable{private Account fromAccount;private Account toAccount;private int amount;private Lock fromAccountLock = new ReentrantLock();private Lock toAccountLock = new ReentrantLock();public TransferAccount2(Account fromAccount, Account toAccount, int amount) {this.fromAccount = fromAccount;this.toAccount = toAccount;this.amount = amount;}@Overridepublic void run() {while(true){//synchronized (fromAccount){if(fromAccountLock.tryLock()){if(toAccountLock.tryLock()){if(fromAccount.getBalance()>=amount) {fromAccount.debit(amount);toAccount.credit(amount);}System.out.println(Thread.currentThread().getName());System.out.println(fromAccount.getCountName()+"->"+toAccount.getCountName()+":"+amount );System.out.println(fromAccount.getCountName() +"账户余额:"+ fromAccount.getBalance());System.out.println(toAccount.getCountName() +"账户余额:"+ toAccount.getBalance());}}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {Account bigHead = new Account("冤大头",100000);Account smallKill = new Account("门小抠",200000);new Thread(new TransferAccount2(bigHead,smallKill,10)).start();new Thread(new TransferAccount2(smallKill,bigHead,20)).start();}}

相关文章:

什么是线程死锁?如何解决死锁问题

死锁&#xff0c;一组互相竞争的资源的线程之间相互等待&#xff0c;导致永久阻塞的现象。 如下图所示&#xff1a; 与死锁对应的&#xff0c;还有活锁&#xff0c;是指线程没有出现阻塞&#xff0c;但是无限循环。 有一个经典的银行转账例子如下&#xff1a; 我们有个账户类…...

C语言几种判断语句简述

C 判断 判断结构要求程序员指定一个或多个要评估或测试的条件&#xff0c;以及条件为真时要执行的语句&#xff08;必需的&#xff09;和条件为假时要执行的语句&#xff08;可选的&#xff09;。 C 语言把任何非零和非空的值假定为 true&#xff0c;把零或 null 假定为 fals…...

【python学习笔记】:SQL常用脚本(二)

11、四舍五入ROUND函数 ROUND ( numeric_expression , length [ ,function ] ) function 必须为 tinyint、smallint 或 int。 如果省略 function 或其值为 0&#xff08;默认值&#xff09;&#xff0c;则将舍入 numeric_expression。 如果指定了0以外的值&#xff0c;则将截…...

【Linux】进程地址空间

文章目录&#x1f3aa; 进程地址空间&#x1f680;1.写时拷贝与虚拟地址&#x1f680;2.地址空间引入&#x1f680;3.地址空间的意义⭐3.1 虚拟地址寻址⭐3.2 虚拟地址意义&#x1f3aa; 进程地址空间 地址空间&#xff08;address space&#xff09;表示任何一个计算机实体所…...

Qt音视频开发17-vlc内核回调拿图片进行绘制

一、前言 在众多播放器中&#xff0c;支持的种类格式众多&#xff0c;并支持DVD影音光盘&#xff0c;VCD影音光盘及各类流式协议&#xff0c;提供了sdk进行开发&#xff0c;这点是至关重要的&#xff0c;尽管很多优秀的播放器很牛逼&#xff0c;由于没有提供sdk第三方开发&…...

安装配置DHCP

本次实验采用CentOS71.检查在安装DHCP之前先使用rpm命令查看系统中已有的DHCP软件包rpm -qa | grep dhcp由此可知&#xff0c;系统中尚未安装DHCP软件包2.安装我们可以使用yum命令为系统安装DHCP软件包yum -y install dhcp安装完成后再次检查可以看到DHCP软件包3.配置dhcp配置文…...

MarkDown中写UML图的方法

目录序UML图之顺序图顺序图的四个要素关于消息箭头的语法Mermaid中顺序图的简单例子样例用小人表示对象为对象设置别名激活对象UML图之类图类图中常见的关系关于不同类型关系的语法Mermaid中类图的简单例子样例类定义的两种方式为类定义成员双向关系的表示多重性关系的表示UML之…...

Axure8设计—动态仪表盘

本次分享的的案例是Axure8制作的动态仪表盘,根据设置的数值&#xff0c;仪表盘指针旋转到相应的值位置 预览地址&#xff1a;https://2qiuwg.axshare.com 下载地址&#xff1a;https://download.csdn.net/download/weixin_43516258/87502161 一、制作原型 1、首先创建空白页…...

【C++】类和对象的六个默认成员函数

类的6个默认成员函数构造函数概念特性析构函数概念特性拷贝构造函数概念特征拷贝构造函数典型调用场景&#xff1a;赋值运算符重载运算符重载赋值运算符重载取地址及const取地址操作符重载类的6个默认成员函数 到底什么是类的6个默认成员函数呢&#xff1f;相信大家一定对此怀…...

4、算法MATLAB---认识矩阵

认识矩阵1、矩阵定义和基本运算1.1 赋值运算符&#xff1a;1.2 等号运算符&#xff1a;1.3 空矩阵1.4 一行一列矩阵1.5 行矩阵&#xff08;元素用空格或逗号分隔&#xff09;1.6 列矩阵&#xff08;分号表示换行&#xff09;1.7 m行n列的矩阵&#xff1a;行值用逗号间隔&#x…...

vue3+rust个人博客建站日记2-确定需求

反思 有人说过我们正在临近代码的终结点。很快&#xff0c;代码就会自动产生出来&#xff0c;不需要再人工编写。程序员完全没用了&#xff0c;因为商务人士可以从规约直接生成程序。 扯淡&#xff01;我们永远抛不掉代码&#xff0c;因为代码呈现了需求的细节。在某些层面上&a…...

Linux安装云原生网关Kong/KongA

目录1 概述2 创建服务器3 安装postgres4 安装kong5 安装node6 安装KONGA1 概述 Kong Kong是一款基于OpenResty&#xff08;NginxLua模块&#xff09;编写的高可用、易扩展的开源API网关&#xff0c;专为云原生和云混合架构而建&#xff0c;并针对微服务和分布式架构进行了特别…...

Vue学习笔记(2)

2.1 事件处理 2.1.1 事件监听器 JavaScript&#xff1a;通过获取DOM对象再往DOM对象上使用addEventListener注册监听事件 const btn document.querySelector(#my-button) btn.addEventListener(click, function() {alert(点击事件!) })jQuery&#xff1a;通过$选择器绑定对象…...

2023年三月份图形化四级打卡试题

活动时间 从2023年3月1日至3月21日&#xff0c;每天一道编程题。 本次打卡的规则如下&#xff1a; 小朋友每天利用10~15分钟做一道编程题&#xff0c;遇到问题就来群内讨论&#xff0c;我来给大家答疑。 小朋友做完题目后&#xff0c;截图到朋友圈打卡并把打卡的截图发到活动群…...

Python操作Excel

Python中对Excel文件的操作包括&#xff1a;读、写、修改。如果要对其进行如上的操作需要导入Python的第三方模块&#xff1a;xlrd、xlwd、xlutils&#xff0c;其分别对应Python的读、写、修改的操作 一、安装Python的第三方模块 二、操作Excel的基本步骤 1、导入响对应的模…...

Codeforces Round #853 (Div. 2) C. Serval and Toxel‘s Arrays【统计次数,算贡献】

链接 传送门 分析 这道题想法其实很简单&#xff0c;样例的计算方法一定要看懂。以样例1为例&#xff0c;根据他的操作方法可以得到两个新的数组&#xff0c;和一个原来的数组&#xff0c;总共三个数组。 1 2 3 4 2 3 4 5 3 他们两两配对去重&#xff0c;求出总的value。由于每…...

微信小程序-1:比较两数的大小

程序来源》微信小程序开发教程&#xff08;第二章&#xff09; 主编&#xff1a;黄寿孟、易芳、陶延涛 ISBN&#xff1a; 9787566720788 程序运行结果&#xff1a; <!--index.wxml--> <view class"container"> <text>第一个数字&#xff1a;&…...

数据结构——树

深度优先/广度优先遍历深度优先&#xff1a;访问根节点对根节点的 children 挨个进行深度优先遍历const tree {val: "a",children: [{val: "b",children: [{val: "d",children: [],},{val: "e",children: [],},],},{val: "c&quo…...

【华为OD机试模拟题】用 C++ 实现 - 找到它(2023.Q1)

最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 去重求和(2023.Q1) 文章目录 最近更新的博客使用说明找到它题目输入输出示例一输入输出示例二输入输出说明Code使用说明 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD …...

python中yield的使用

在 Python 中&#xff0c;yield 是一个关键字&#xff0c;它用于定义生成器函数。生成器函数是一个特殊的函数&#xff0c;可以返回一个迭代器&#xff0c;当生成器函数被调用时&#xff0c;它不会立即执行&#xff0c;而是返回一个生成器对象&#xff0c;通过迭代生成器对象可…...

基于距离变化能量开销动态调整的WSN低功耗拓扑控制开销算法matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.算法仿真参数 5.算法理论概述 6.参考文献 7.完整程序 1.程序功能描述 通过动态调整节点通信的能量开销&#xff0c;平衡网络负载&#xff0c;延长WSN生命周期。具体通过建立基于距离的能量消耗模型&am…...

【力扣数据库知识手册笔记】索引

索引 索引的优缺点 优点1. 通过创建唯一性索引&#xff0c;可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度&#xff08;创建索引的主要原因&#xff09;。3. 可以加速表和表之间的连接&#xff0c;实现数据的参考完整性。4. 可以在查询过程中&#xff0c;…...

【JVM】- 内存结构

引言 JVM&#xff1a;Java Virtual Machine 定义&#xff1a;Java虚拟机&#xff0c;Java二进制字节码的运行环境好处&#xff1a; 一次编写&#xff0c;到处运行自动内存管理&#xff0c;垃圾回收的功能数组下标越界检查&#xff08;会抛异常&#xff0c;不会覆盖到其他代码…...

React19源码系列之 事件插件系统

事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案

一、TRS收益互换的本质与业务逻辑 &#xff08;一&#xff09;概念解析 TRS&#xff08;Total Return Swap&#xff09;收益互换是一种金融衍生工具&#xff0c;指交易双方约定在未来一定期限内&#xff0c;基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...

C++使用 new 来创建动态数组

问题&#xff1a; 不能使用变量定义数组大小 原因&#xff1a; 这是因为数组在内存中是连续存储的&#xff0c;编译器需要在编译阶段就确定数组的大小&#xff0c;以便正确地分配内存空间。如果允许使用变量来定义数组的大小&#xff0c;那么编译器就无法在编译时确定数组的大…...

云原生安全实战:API网关Kong的鉴权与限流详解

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、基础概念 1. API网关&#xff08;API Gateway&#xff09; API网关是微服务架构中的核心组件&#xff0c;负责统一管理所有API的流量入口。它像一座…...

免费数学几何作图web平台

光锐软件免费数学工具&#xff0c;maths,数学制图&#xff0c;数学作图&#xff0c;几何作图&#xff0c;几何&#xff0c;AR开发,AR教育,增强现实,软件公司,XR,MR,VR,虚拟仿真,虚拟现实,混合现实,教育科技产品,职业模拟培训,高保真VR场景,结构互动课件,元宇宙http://xaglare.c…...

MacOS下Homebrew国内镜像加速指南(2025最新国内镜像加速)

macos brew国内镜像加速方法 brew install 加速formula.jws.json下载慢加速 &#x1f37a; 最新版brew安装慢到怀疑人生&#xff1f;别怕&#xff0c;教你轻松起飞&#xff01; 最近Homebrew更新至最新版&#xff0c;每次执行 brew 命令时都会自动从官方地址 https://formulae.…...

WebRTC从入门到实践 - 零基础教程

WebRTC从入门到实践 - 零基础教程 目录 WebRTC简介 基础概念 工作原理 开发环境搭建 基础实践 三个实战案例 常见问题解答 1. WebRTC简介 1.1 什么是WebRTC&#xff1f; WebRTC&#xff08;Web Real-Time Communication&#xff09;是一个支持网页浏览器进行实时语音…...