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

news2024/11/26 4:44:39

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

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


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

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

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;
    }


    @Override
    public 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;
    }


    @Override
    public 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;
    }


    @Override
    public 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();

    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/373385.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

操作系统权限提升(十四)之绕过UAC提权-基于白名单AutoElevate绕过UAC提权

系列文章 操作系统权限提升(十二)之绕过UAC提权-Windows UAC概述 操作系统权限提升(十三)之绕过UAC提权-MSF和CS绕过UAC提权 注&#xff1a;阅读本编文章前&#xff0c;请先阅读系列文章&#xff0c;以免造成看不懂的情况&#xff01;&#xff01; 基于白名单AutoElevate绕过…

2-MATLAB APP Design-下拉菜单栏的使用

一、APP 界面设计展示 1.新建一个空白的APP,在此次的学习中,我们会用到编辑字段(文本框)、下拉菜单栏、坐标区,首先在界面中拖入一个编辑字段(文本框),在文本框中输入内容:下拉菜单栏的使用,调整背景颜色,字体的颜色为黑色,字体的大小调为26. 2.在左侧组件库常用栏…

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

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

【网络编程套接字(一)】

网络编程套接字&#xff08;一&#xff09;理解源IP地址和目的IP地址理解源MAC地址和目的MAC地址理解源端口号和目的端口号PORT VS PID认识TCP协议和UDP协议网络字节序socket编程接口socket常见APIsockaddr结构简单的UDP网络程序服务端创建套接字服务端绑定字符串IP VS 整数IP客…

面试官: 你知道 JWT、JWE、JWS 、JWK嘛?

想起了 之前做过的 很多 登录授权 的项目 它相比原先的session、cookie来说&#xff0c;更快更安全&#xff0c;跨域也不再是问题&#xff0c;更关键的是更加优雅 &#xff0c;所以今天总结了一篇文章来介绍他 JWT 指JSON Web Token&#xff0c;如果在项目中通过 jjwt 来支持 J…

Springboot整合 Thymeleaf增删改查一篇就够了

很早之前写过Thymeleaf的文章&#xff0c;所以重新温习一下&#xff0c;非前后端分离&#xff0c;仅仅只是学习 官网&#xff1a; https://www.thymeleaf.org/ SpringBoot可以快速生成Spring应用&#xff0c;简化配置&#xff0c;自动装配&#xff0c;开箱即用。 JavaConfigur…

【java基础】枚举类(enum)

文章目录基本介绍快速使用字段、方法、构造器枚举类方法toString方法valueOf方法values方法ordinal方法基本介绍 在java中有一种特殊的类型就是枚举类&#xff0c;对于一个有限的有固定值的集合&#xff0c;我们就可以考虑使用枚举类来进行表示&#xff0c;例如服装的大小为 小…

linux shell 入门学习笔记15 shell 条件测试

概念 shell的条件测试目的是得出真和假。 shell 提供的条件测试语法 test 命令 [] 中括号命令 语法*&#xff1a; test条件测试 test命令用来评估一个表达式&#xff0c;他的结果是真&#xff0c;还是假&#xff0c;如果条件为真&#xff0c;那么命令执行状态结果就为0&…

【蓝桥杯集训·周赛】AcWing 第92场周赛

文章目录第一题 AcWing 4864. 多边形一、题目1、原题链接2、题目描述二、解题报告1、思路分析2、时间复杂度3、代码详解第二题 AcWing 4865. 有效类型一、题目1、原题链接2、题目描述二、解题报告1、思路分析2、时间复杂度3、代码详解第三题 AcWing 4866. 最大数量一、题目1、原…

Spring之丐版IOC实现

文章目录IOC控制反转依赖注入Bean的自动装配方式丐版IOC实现BeanDefinition.javaResourceLoader.javaBeanRegister.javaBean和DI的注解BeanFactory.javaApplicationContext测试&#xff0c;实现在这里插入图片描述大家好&#xff0c;我是Leo。Spring核心中依赖注入和IOC容器是非…

JWT令牌

1.普通令牌的问题 客户端申请到令牌&#xff0c;接下来客户端携带令牌去访问资源&#xff0c;到资源服务器将会校验令牌的合法性。 从第4步开始说明&#xff1a; 1、客户端携带令牌访问资源服务获取资源。 2、资源服务远程请求认证服务校验令牌的合法性 3、如果令牌合…

QML Image and Text(图像和文字)

Image&#xff08;图片&#xff09; 图像类型显示图像。 格式&#xff1a; Image {source: "资源地址" } source&#xff1a;指定资源的地址 自动检测文件拓展名&#xff1a;source中的URL 指示不存在的本地文件或资源&#xff0c;则 Image 元素会尝试自动检测文件…

MVI 架构更佳实践:支持 LiveData 属性监听

前言MVI架构为了解决MVVM在逻辑复杂时需要写多个LiveData(可变不可变)的问题,使用ViewState对State集中管理&#xff0c;只需要订阅一个 ViewState 便可获取页面的所有状态通过集中管理ViewState&#xff0c;只需对外暴露一个LiveData&#xff0c;解决了MVVM模式下LiveData膨胀…

Linux_vim编辑器入门级详细教程

前言&#xff08;1&#xff09;vim编辑器其实本质上就是对文本进行编辑&#xff0c;比如在.c文件中改写程序&#xff0c;在.txt文件写笔记什么的。一般来说&#xff0c;我们可以在windows上对文本进行编译&#xff0c;然后上传给Linux。但是有时候我们可能只是对文本进行简单的…

MySQL运维

目录 1、日志 1、错误日志 2、二进制日志 3、查询日志 4、慢查询日志 2、主从复制 搭建 1、主库配置 2、从库配置 3、分库分表 1、简介 ​编辑 1、垂直拆分 2、水平拆分 3、实现技术 2、MyCat 3、MyCat使用和配置 配置 4、MyCat分片 1、垂直拆分 2、水平拆分…

线材-电子线载流能力

今天来讲的是关于电子线的一个小知识&#xff0c;可能只做板子的工程师遇到此方面的问题会比较少&#xff0c;做整机的工程师则必然会遇到此方面问题&#xff0c;那就是线材问题。 下面主要说下电子线的过电流能力。&#xff08;文末有工具下载&#xff09;电子线&#xff08;h…

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

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

CTFer成长之路之Python中的安全问题

Python中的安全问题CTF 1.Python里的SSRF 题目提示 尝试访问到容器内部的 8000 端口和 url path /api/internal/secret 即可获取 flag 访问url&#xff1a; http://f5704bb3-5869-4ecb-9bdc-58b022589224.node3.buuoj.cn/ 回显如下&#xff1a; 通过提示构造payload&…

Pytorch复习笔记--Conv2d和Linear的参数量和显存占用量比较

目录 1--nn.Conv2d()参数量的计算 2--nn.Linear()参数量计算 3--显存占用量比较 1--nn.Conv2d()参数量的计算 conv1 nn.Conv2d(in_channels3, out_channels64, kernel_size1) 计算公式&#xff1a; Param in_c * out_c * k * k out_c; in_c 表示输入通道维度&#xff1b…

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

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