死锁细究!

news2024/11/28 2:37:44

一、死锁的定义&危害

1、死锁是什么

  • 发生在并发
  • 互不想让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁。

  • 多个线程造成死锁的情况
    • 如果多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能会发送死锁

2、死锁的影响

死锁的影响在不同的系统中是不一样的,这取决于系统对死锁的处理能力

  • 数据库中: 数据库可以检测到并介入,可以通过放弃其中某个事物的锁来解决
  • JVM中: JVM 无法自动处理锁,需要人工处理

3、死锁的危害

死锁发生的几率不高但危害大,一旦发生,多是高并发场景,影响用户多;甚至可能造成整个系统奔溃,或者子系统崩溃,或者性能降低;而且在压力测试过程无法找出所有潜在的死锁。

二、发生死锁的例子

1. 两个线程的简单场景

代码展示:

/**
  *  必定发生死锁的情况
 */
public class MustDeadLock implements Runnable {
    int flag = 1;//标记位
    static Object lock1 = new Object();
    static Object lock2 = new Object();

    public static void main(String[] args) {
        MustDeadLock r1 = new MustDeadLock();
        MustDeadLock r2 = new MustDeadLock();
        r1.flag=1;
        r2.flag=0;
        Thread thread1 = new Thread(r1);
        Thread thread2 = new Thread(r2);

        thread1.start();
        thread2.start();
    }

    @Override
    public void run() {
        System.out.println("flag= " + flag);
        if (flag == 1) {
            synchronized (lock1){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    System.out.println("线程1成功拿到两把锁");
                }
            }
        }
        if (flag == 0) {
            synchronized (lock2){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1){
                    System.out.println("线程2成功拿到两把锁");
                }
            }

        }
    }

}

结果如下:

thread1 启动之后拿到 lock1 并休眠 500ms ,与此同时,thread2 启动之后拿到了 lock2 并休眠 500ms ;thread1 醒来之后想去获取 lock2 ,但是 lock2 被 thread2 拥有,因此 thread1 会陷入阻塞,而 thread2 醒来之后会去拿 lock1 ,但是 lock 在 thread1 手上,thread2 也会陷入阻塞, thread1 和 thread2 都进入了阻塞,相互等待,结果进入了死锁。

2. 银行转账的案例

具体案例可参考文章:《银行转账(死锁)》

三、死锁发生的必要条件

1、互斥条件

一个资源每一次只能被一个进程或者线程使用。比如我这里有锁,我拿了这锁之后,其他线程就不能拿到他,除非我释放了他

2、请求与保持条件

第一个线程请求第二把锁同时保持第一把锁。比如线程1想要请求一把锁B,在请求期间,他对已经拿到的锁A一直保持不释放

3、不剥夺条件

不被外界干扰剥夺锁。即没有外界干扰结束死锁的情况。

4、循环等待条件

两个或者多个互相等待或者环形等待锁的释放。比如两个线程等待条件,就是你等我释放,我等你释放;多个线程等待条件,就是A等B,B等C,C等D,D等E,E等F,F等A,无尽不释放,无尽陷入等待,构成环路。

以上四个条件:缺一不可, 只需要破解以上4个条件任意一个条件,这个死锁就不会发生了!!

四、如何定位死锁的位置

1、jstack 命令

该代码来自文章《银行转账(死锁)》中,代码如下:

public class TransferMoney implements Runnable {

    Integer flag = 1;

    static Account a = new Account(1000);
    static Account b = new Account(1000);

    //主函数
    public static void main(String[] args) throws InterruptedException {
        TransferMoney t1 = new TransferMoney();
        TransferMoney t2 = new TransferMoney();
        t1.flag = 1;
        t1.flag = 0;

        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);

        thread1.start();
        thread2.start();
        thread1.join();
        thread1.join();
        System.out.println("a的余额为:"+a.balance);
        System.out.println("a的余额为:"+b.balance);
    }
    
    @Override
    public void run() {
        if (flag == 1) {
            transferMoney(a, b, 500);
        }
        if (flag == 0) {
            transferMoney(b, a, 500);
        }
    }
    
    // 转账
    private void transferMoney(Account from, Account to, int amount) {
        synchronized (from) {
            System.out.println(Thread.currentThread().getName()+"获取到第一把锁");
            //加入线程睡眠500ms
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (to) {
                System.out.println(Thread.currentThread().getName()+"获取到第二把锁");
                if (from.balance - amount < 0) {
                    System.out.println("余额不足,转账失败。");
                    return;
                }

                from.balance -= amount;
                to.balance += amount;
                System.out.println("成功转账" + i + "元");
            }
        }
    }

    static class Account {
        //余额
        int balance;

        public Account(int balance) {
            this.balance = balance;
        }
    }
}

下面通过使用java自带的jstack命令,来查找我们项目中的死锁问题

## 需要首先获取程序的进程 pid
jps
## 然后在 终端界面执行如下命令
jstack 8359  #javahome下的jastack命令 进程的pid

执行结果图:

可以清晰地看到 Thread-1 拿到了锁 <0x000000076adae688> ,正在等待 <0x000000076adae678>,而 Thread-0 拿到了锁 <0x000000076adae678>,正在等待 <0x000000076adae688>,于是两者互相等待,造成死锁。

2、ThreadMXBean代码

/**
 *      通过 ThreadMXBean 检测死锁
 */
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class ThreadMXBeanDetection implements Runnable{
    int flag = 1;//标记位
    static Object lock1 = new Object();
    static Object lock2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();
        ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();
        r1.flag=1;
        r2.flag=0;
        Thread thread1 = new Thread(r1);
        Thread thread2 = new Thread(r2);

        thread1.start();
        thread2.start();
        Thread.sleep(1000);

        //得到实例
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        //发现死锁
        if (deadlockedThreads != null && deadlockedThreads.length>0){
            //迭代
            for (long item : deadlockedThreads) {
                //获取线程信息
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(item);
                //获取死锁线程的名字
                System.out.println("发现死锁:"+threadInfo.getThreadName());
            }
        }
    }

    @Override
    public void run() {
        System.out.println("flag= " + flag);
        if (flag == 1) {
            synchronized (lock1){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    System.out.println(flag);
                }
            }
        }
        if (flag == 0) {
            synchronized (lock2){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1){
                    System.out.println(flag);
                }
            }

        }
    }
}

打印结果:

如上图所示,ThreadMXBean 可以检测死锁,如果我们检测到了之后,就可以编写对应的逻辑,比如重启线程、通知告警系统、发消息提醒运维人员等。

五、修复死锁策略

1、线上发生死锁应该怎么办

保存案发现场,然后立即重启服务器,确保线上服务能运行下去,之后在利用保存的信息,排查死锁,修改代码,重新发布。

2、常见修复策略

  • 避免策略哲学家就餐的换手方案、转账换序方案。可参考下面两篇文章:

    • 《哲学家就餐问题》
    • 《银行转账》
  • 检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁

    • 检测算法: 锁的调用链路图
      • 允许发生死锁
      • 每次调用锁的记录,并绘成一个有向的数据结构图
      • 定期检查'锁的调用链路图'中是否存在环路
      • 一旦发现死锁,就用死锁恢复机制恢复
    • 死锁恢复机制
      • 进程终止:逐个终止线程,直到死锁消除
        • 终止顺序:
          • 线程的优先级(是前台交互还是后台处理);
          • 已占用资源多少、还需要的资源多少;
          • 已运行时间长短
      • 资源抢占:
        • 把已经分发出去的锁收回
        • 让线程回退让几步,这样就不用结束整个线程,成本比较低
          • 缺点:可能造成线程饥饿
  • 鸵鸟策略:鸵鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而鸵鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复。这是一种消极的处理方式,不推荐。

六、如何在工程中避免死锁

1、设置超时时间

  • 由于 synchronized不具备尝试锁的能力,可以使用 Lock的tryLock(long timeout, TimeUnit unit) 替代。
  • 造成超时的可能性多:发生了死锁、线程陷入死循环、线程执行很慢,无论是哪种情况,只要过了超时时间,就认为失败
  • 获取锁失败的时候,要执行一些应对措施:比如打日志、发报警邮件、重启等
/**
 *      使用Lock中的tryLock()避免死锁
 */
public class TryLockDeaLock implements Runnable {
    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1){
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)){
                        System.out.println("线程1,拿到lock1");
                        Thread.sleep(new Random().nextInt(1000));
                        if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){
                            System.out.println("线程1,成功获取到 lock1 & lock2");
                            //释放
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        }else {
                            System.out.println("线程1,获取lock2,失败,已重试");
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    }else {
                        System.out.println("线程1,获取lock1,失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (flag ==0){
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)){
                        System.out.println("线程2,拿到lock2");
                        Thread.sleep(new Random().nextInt(1000));
                        if (lock1.tryLock(3000,TimeUnit.MILLISECONDS)){
                            System.out.println("线程2,成功获取到 lock1 & lock2");
                            //释放
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        }else {
                            System.out.println("线程2,获取lock1,失败,已重试");
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    }else {
                        System.out.println("线程2,获取lock2,失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //主函数
    public static void main(String[] args) {
        TryLockDeaLock t1 = new TryLockDeaLock();
        TryLockDeaLock t2 = new TryLockDeaLock();
        t1.flag = 1;
        t2.flag = 0;
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);

        thread1.start();
        thread2.start();
    }
}

2、多使用并发工具类,而不是自己设计锁

  • ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等
  • 实际应用中java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock更高
  • 多用并发集合少用同步集合,并发集合比同步集合的可扩展性更好
  • 并发场景需要用到map,首先想到用ConcurrentHashMap

3、尽量降低锁的使用粒度:

用不同的锁而不是一个锁

4、如果能使用同步代码块,就不使用同步方法

在代码块中,可以自己指定锁对象(方便掌控锁)

5、给你的线程起个有意义的名字

如果遇到问题,在debug和排查时事倍功半,更容易定位到问题,框架和JDK都遵守这个最佳实践

6、避免锁嵌套

比如下图中在已经持有 lock1 的情况下,还要继续请求 lock2,锁互相嵌套,更容易导致死锁

7、分配资源前先看能不能收回

银行家算法:在放贷之前,首先对资源的进行有效计算,看看资源能不能收回来。

8、不要几个功能用同一把锁

每一个功能都要有自己专门的锁,专锁专用

七. 其它线程活跃性故障:活锁、饥饿

死锁是最常见的活跃性问题,不过除了之前的死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为 活跃性问题

1. 活锁

(1)什么是活锁

  • 虽然线程并没有阻塞,也始终在运行(所以被称为“活锁”,线程是“活的”),但是程序却得不到进展,因为线程始终重复做同样的事
  • 如果是死锁,只会是等待,不会消耗CPU资源,但活锁除了程序无法进行,还会消耗 cpu 资源

(2)代码演示

可参考文章:《牛郎织女的幸福生活》

(3)生产中的活锁:消息队列

如果在消息队列中使用策略:消息处理失败,就放在队列开头重试。但是若该消息失败的原因是由于某个依赖的服务出了问题,会导致该消息一直失败。那么消息就会不停地重新放回队列头,不停地重试,虽然没阻塞,但是由于一直在处理该消息,程序也无法继续执行。

解决方案:

  • 放到队列尾部
  • 设置重试次数限制

(4)活锁问题突破口

增加随机因素:出现活锁的原因大都是因为重试机制不变,每一次重试的触发时机总是相同导致。所以可以参考以太网的指数退避算法:连接重试时间不是固定的,而是随机的,并且随机范围随着碰撞强度的升高而逐渐扩大。我们也可以加入一些随机因素,改变每次重试的条件或时机,或者每次重试的执行逻辑等。

2. 饥饿

(1)什么是饥饿

  • 当线程需要某些资源(例如CPU),但是却一直始终得不到
  • 线程的优先级设置过低,或者有某个线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁

(2)饥饿的影响

饥饿可能会导致响应性差:比如,我们的浏览器有一个线程负责处理前台响应(打开收藏夹等动作),另外的后台线程负责下载图片和文件、计算渲染等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好地执行,这会导致用户的体验很差

(3)解决方案

  • 注意到程序上不应该有锁使用完却不释放的逻辑错误
  • 不应该在程序中设置优先级

文章来源:死锁细究

个人微信:CaiBaoDeCai

微信公众号名称:Java知者

微信公众号 ID: JavaZhiZhe

谢谢关注!

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

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

相关文章

学生成绩管理系统(C语言)

学生成绩管理系统 一、实现思路二、代码的实现&#xff08;1&#xff09;构造功能框架&#xff08;2&#xff09;实现各项功能 三、完整的代码四、总结 本篇博客介绍一个关于学生成绩管理系统的C语言代码&#xff0c;包含读取成绩、计算各门课程的总分和平均分、按分数排序、按…

快速实现一个分布式定时器

定时器&#xff08;Timer&#xff09;是一种在业务开发中常用的组件&#xff0c;主要用在执行延时通知任务上。本文以笔者在工作中的实践作为基础&#xff0c;介绍如何使用平时部门最常用的组件快速实现一个业务常用的分布式定时器服务。同时介绍了过程中遇到问题的一些解决方案…

手机越狱:探索自由与风险的边界

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c;主要职责&#xff1a;测试开发、CI/CD 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。&#x1f60a; 座右铭&#xff1a;不想…

100天精通Golang(基础入门篇)——第5天: Go语言中的数据类型学习

&#x1f337; 博主 libin9iOak带您 Go to Golang Language.✨ &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &#x1f30a; 《I…

htmlCSS-----盒模型

目录 前言&#xff1a; 盒子 1.内容区域 2. 边框 3. 内边距区域 4. 外边距区域&#xff08;margin&#xff09; 怪异盒模型 前言&#xff1a; 前面我们学习了CSS中的选择器的使用方法&#xff0c;那这一节我们就学习CSS中的盒模型&#xff0c;通过盒模型我们可以去更好的…

uni-app引入html2canvas截图以及截长图

下载安装html2canvas 方式一&#xff0c;https://www.bootcdn.cn/ CDN网站下载html2canvas插件 这里下载后放在测项目目录common下面 页面中引入 方式二、npm方式安装html2canvas 1、npm方式下载 npm i html2canvas2、引入html2canvas import html2canvas from html2can…

数据结构——广义表

文章目录 前言二、特殊矩阵的压缩存储数组的存储结构和实现按行优先存储按列优先存储 矩阵的压缩存储稀疏矩阵 广义表 总结 前言 数组&#xff0c;数组的压缩存储&#xff0c;广义表 二、特殊矩阵的压缩存储 数组的存储结构和实现 对于多维数组&#xff0c;可以分为按行优先…

UnityVR--组件9--视频组件VideoPlayer

目录 前言 参数解释 RenderMode渲染方式 VideoPlayer类中的API 前言 在之前的VR场景中已经使用过VideoPlayer播放视频&#xff08;Unity.UI的交互&#xff08;6&#xff09;-播放视频&#xff09;&#xff0c;不过在VR中设置是有些不同的&#xff0c;这里更详细地说明一下V…

8.面向对象编程(高级部分)|Java学习笔记

文章目录 类变量和类方法类变量类变量使用注意事项和细节 类方法类方法使用注意事项和细节 理解 main 方法语法代码块代码块使用注意事项和细节讨论 单例设计模式单例模式应用实例饿汉式 VS 懒汉式 final 关键字final 使用注意事项和细节 抽象类抽象类的介绍抽象类使用的注意事…

软件测试|测试金字塔是什么,它的目的是什么,以及它包含哪些层次?

一、测试金字塔的概念&#xff1a; 测试金字塔是2009年Mike Cohn在他的著作《Succeeding with Agile》一书正式提出的。他是一个类比的概念&#xff0c;形容每一层&#xff0c;或者说不同集成阶段测试覆盖率和知行效率之间的一个相对关系。 测试金字塔最初的原型分三层&#…

chatgpt赋能python:Python循环间隔-了解如何在循环中增加延时

Python循环间隔 - 了解如何在循环中增加延时 在Python编程中&#xff0c;循环是非常常见且重要的控制语句。 它使我们可以多次执行代码块。 但是&#xff0c;在有些情况下&#xff0c;您可能需要在循环之间增加一定的延时时间。 这就是Python循环间隔的概念。 在本文中&#x…

初次使用PPYOLOE-R

目的&#xff1a;优化基于yolov5-obb旋转目标检测算法的证件区域检测&#xff0c;之前的方法是基于anchor&#xff0c;每次使用都要调试anchor&#xff1b;而ppyoloe-r是free anchor的算法&#xff1b; 源码位置&#xff1a;https://github.com/PaddlePaddle/PaddleDetection/…

学成在线----day2

1、mybatis-plus分页 pom: <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.ap…

【大数据之Hive】九、Hive之DDL(Data Definition Language)数据定义语言

1 数据库 [ ] 里的都是可选的操作。 1.1 创建数据库 语法&#xff1a; create database [if not exists] database_name [comment database_comment(注释)] [location hdfs_path] [with dbproperties (property_name-propertyproperty_value,...)]; 如&#xff1a; creat…

真实性能测试案例之性能分析与报告

今天主要跟大家分享一个曾经所做的真实性能测试的案例&#xff0c;主要为其中性能测试分析报告过程部分&#xff0c;希望能对你以后怎么做性能分析和报告有所帮助。这个案例的测试目的为&#xff1a;在线考试为“XX平台”中的一个重要模块&#xff0c;根据目前业务的需要&#…

ChatGPT实用使用指南 让它解你所问

Chatgpt无疑是这几年来影响力最大的AI技术之一&#xff0c;生成式的AI模型正在促进各个行业的效率和自动化发展&#xff0c;Chatgpt对于个人、企业和各个行业都有着一定的影响 在我刚接触的时候&#xff0c;发现对Chatgpt的认知太肤浅了&#xff0c;一个最强的ai聊天机器人摆在…

实时检测Aruco标签坐标及位姿opencv-python4.6和4.7版本

先说opencv-contrib-python4.7.0.72时&#xff0c;aruco下面带曲线&#xff0c;但是程序也能跑&#xff0c;可以跑检测的&#xff0c;对比4.6版本需要改三个函数 4.6装opencv-contrib-python 4.7装opencv-contrib-python 1 cv2.aruco.Dictionary_get() cv2.aruco.getPredef…

Smali的使用技巧:快速定位Android应用程序中的关键代码

简述 Smali是一种Android应用程序的Dalvik虚拟机指令集汇编语言&#xff0c;用于编写和修改应用程序的DEX文件。通过编写和修改Smali代码&#xff0c;可以实现对Android应用程序的定制化和逆向分析。Smali语言类似于汇编语言&#xff0c;直接操作Dalvik虚拟机指令集。 Smali代…

2023最火的软件测试面试宝典,你刷过没?

这是一份最近疯传的软件测试面试宝典&#xff0c;你有刷过吗&#xff1f; 面试宝典一共400页&#xff0c;包括了测试基础102页&#xff0c;Linux基础38页&#xff0c;MySQL63页&#xff0c;web测试21页&#xff0c;app测试38页&#xff0c;selenium相关50页&#xff0c;性能测试…

02.引擎架构分类

简介 1.工具层 2.功能层&#xff1a;绘制、渲染、让世界里面的东西能看见、动起来 3.资源层&#xff1a;负责加载大量的数据和文件 4.核心层&#xff1a;游戏引擎的瑞士军刀&#xff0c;各种功能处理的内核 5.平台层&#xff1a;用于适配游戏不同的发行平台 第三方中间插…