首页 > 其他分享 > 并发中致命的死锁

并发中致命的死锁

时间:2023-06-01 14:01:08浏览次数:36  
标签:致命 threadB threadA resourceA resourceB 并发 死锁 线程

本文首发自「慕课网」,想了解更多IT干货内容,程序员圈内热闻,欢迎关注"慕课网"或慕课网公众号!

作者:王军伟Tech| | 慕课网讲师


1. 前言

本文内容主要是对死锁进行深入的讲解,具体内容点如下:

  • 理解线程的上下文切换,这是本节的辅助基础内容,从概念层面进行理解即可;
  • 了解什么是线程死锁,在并发编程中,线程死锁是一个致命的错误,死锁的概念是本节的重点之一;
  • 了解线程死锁的必备 4 要素,这是避免死锁的前提,了解死锁的必备要素,才能找到避免死锁的方式;
  • 掌握死锁的实现,通过代码实例,进行死锁的实现,深入体会什么是死锁,这是本节的重难点之一;
  • 掌握如何避免线程死锁,我们能够实现死锁,也可以避免死锁,这是本节内容的核心。

2. 理解线程的上下文切换

概述:在多线程编程中,线程个数一般都大于 CPU 个数,而每个 CPU 同一时-刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。

 并发中致命的死锁_System

定义:当前线程使用完时间片后,就会处于就绪状态并让出 CPU,让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。

问题点解析:那么就有一个问题,让出 CPU 的线程等下次轮到自己占有 CPU 时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场, 当再次执行时根据保存的执行现场信息恢复执行现场。

线程上下文切换时机: 当前线程的 CPU 时间片使用完或者是当前线程被其他线程中断时,当前线程就会释放执行权。那么此时执行权就会被切换给其他的线程进行任务的执行,一个线程释放,另外一个线程获取,就是我们所说的上下文切换时机。

3. 什么是线程死锁

定义:死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

 并发中致命的死锁_死锁_02

如上图所示死锁状态,线程 A 己经持有了资源 2,它同时还想申请资源 1,可是此时线程 B 已经持有了资源 1 ,线程 A 只能等待。

反观线程 B 持有了资源 1 ,它同时还想申请资源 2,但是资源 2 已经被线程 A 持有,线程 B 只能等待。所以线程 A 和线程 B 就因为相互等待对方已经持有的资源,而进入了死锁状态。

4. 线程死锁的必备要素

  • 互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待;
  • 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放,如 yield 释放 CPU 执行权);
  • 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放;
  • 循环等待条件:指在发生死锁时,必然存在一个线程请求资源的环形链,即线程集合 {T0,T1,T2,…Tn}中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占用的资源,以此类推,Tn 正在等待己被 T0 占用的资源。

如下图所示

 并发中致命的死锁_死锁_03

5. 死锁的实现

为了更好的了解死锁是如何产生的,我们首先来设计一个死锁争夺资源的场景。
场景设计

  • 创建 2 个线程,线程名分别为 threadA 和 threadB;
  • 创建两个资源, 使用 new Object () 创建即可,分别命名为 resourceA 和 resourceB;
  • threadA 持有 resourceA 并申请资源 resourceB;
  • threadB 持有 resourceB 并申请资源 resourceA ;
  • 为了确保发生死锁现象,请使用 sleep 方法创造该场景;
  • 执行代码,看是否会发生死锁。

期望结果:发生死锁,线程 threadA 和 threadB 互相等待。

Tips:此处的实验会使用到关键字 synchronized,后续小节还会对关键字 synchronized 单独进行深入讲解,此处对 synchronized 的使用仅仅为初级使用,有 JavaSE 基础即可。

实例

public class DemoTest{
    private static  Object resourceA = new Object();//创建资源 resourceA
    private static  Object resourceB = new Object();//创建资源 resourceB

    public static void main(String[] args) throws InterruptedException {
        //创建线程 threadA
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    System.out.println(Thread.currentThread().getName() + "获取 resourceA。");
                    try {
                        Thread.sleep(1000); // sleep 1000 毫秒,确保此时 resourceB 已经进入run 方法的同步模块
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "开始申请 resourceB。");
                    synchronized (resourceB) {
                        System.out.println (Thread.currentThread().getName() + "获取 resourceB。");
                    }
                }
            }
        });
        threadA.setName("threadA");
        //创建线程 threadB
        Thread threadB = new Thread(new Runnable() { //创建线程 1
            @Override
            public void run() {
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread().getName() + "获取 resourceB。");
                    try {
                        Thread.sleep(1000); // sleep 1000 毫秒,确保此时 resourceA 已经进入run 方法的同步模块
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "开始申请 resourceA。");
                    synchronized (resourceA) {
                        System.out.println (Thread.currentThread().getName() + "获取 resourceA。");
                    }
                }
            }
        });
        threadB.setName("threadB");

        threadA. start();
        threadB. start();
    }
}

代码讲解

  • 从代码中来看,我们首先创建了两个资源 resourceA 和 resourceB;
  • 然后创建了两条线程 threadA 和 threadB。threadA 首先获取了 resourceA ,获取的方式是代码 synchronized (resourceA) ,然后沉睡 1000 毫秒;
  • 在 threadA 沉睡过程中, threadB 获取了 resourceB,然后使自己沉睡 1000 毫秒;
  • 当两个线程都苏醒时,此时可以确定 threadA 获取了 resourceA,threadB 获取了 resourceB,这就达到了我们做的第一步,线程分别持有自己的资源;
  • 那么第二步就是开始申请资源,threadA 申请资源 resourceB,threadB 申请资源 resourceA 无奈 resourceA 和 resourceB 都被各自线程持有,两个线程均无法申请成功,最终达成死锁状态。

执行结果验证

threadA 获取 resourceA。
threadB 获取 resourceB。
threadA 开始申请 resourceB。
threadB 开始申请 resourceA。


看下验证结果,发现已经出现死锁,threadA 申请 resourceB,threadB 申请 resourceA,但均无法申请成功,死锁得以实验成功。

6. 如何避免线程死锁

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,学过操作系统的读者应该都知道,目前只有请求并持有和环路等待条件是可以被破坏的。

造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可避免死锁。

我们依然以第 5 个知识点进行讲解,那么实验的需求和场景不变,我们仅仅对之前的 threadB 的代码做如下修改,以避免死锁。

代码修改

Thread threadB = new Thread(new Runnable() { //创建线程 1
            @Override
            public void run() {
                synchronized (resourceA) { //修改点 1
                    System.out.println(Thread.currentThread().getName() + "获取 resourceB。");//修改点 3
                    try {
                        Thread.sleep(1000); // sleep 1000 毫秒,确保此时 resourceA 已经进入run 方法的同步模块
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "开始申请 resourceA。");//修改点 4
                    synchronized (resourceB) { //修改点 2
                        System.out.println (Thread.currentThread().getName() + "获取 resourceA。"); //修改点 5
                    }
                }
            }
        });

请看如上代码示例,有 5 个修改点:

  • 修改点 1 :将 resourceB 修改成 resourceA;
  • 修改点 2 :将 resourceA 修改成 resourceB;
  • 修改点 3 :将 resourceB 修改成 resourceA;
  • 修改点 4 :将 resourceA 修改成 resourceB;
  • 修改点 5 :将 resourceA 修改成 resourceB。

请读者按指示修改代码,并从新运行验证。

修改后代码讲解

  • 从代码中来看,我们首先创建了两个资源 resourceA 和 resourceB;
  • 然后创建了两条线程 threadA 和 threadB。threadA 首先获取了 resourceA ,获取的方式是代码 synchronized (resourceA) ,然后沉睡 1000 毫秒;
  • 在 threadA 沉睡过程中, threadB 想要获取 resourceA ,但是 resourceA 目前正被沉睡的 threadA 持有,所以 threadB 等待 threadA 释放 resourceA;
  • 1000 毫秒后,threadA 苏醒了,释放了 resourceA ,此时等待的 threadB 获取到了 resourceA,然后 threadB 使自己沉睡 1000 毫秒;
  • threadB 沉睡过程中,threadA 申请 resourceB 成功,继续执行成功后,释放 resourceB;
  • 1000 毫秒后,threadB 苏醒了,继续执行获取 resourceB ,执行成功。

执行结果验证

threadA 获取 resourceA。
threadA 开始申请 resourceB。
threadA 获取 resourceB。
threadB 获取 resourceA。
threadB 开始申请 resourceB。
threadB 获取 resourceB。

我们发现 threadA 和 threadB 按照相同的顺序对 resourceA 和 resourceB 依次进行访问,避免了互相交叉持有等待的状态,避免了死锁的发生。

总结

死锁是并发编程中最致命的问题,如何避免死锁,是并发编程中恒久不变的问题。
掌握死锁的实现以及如果避免死锁的发生,是本文内容的重中之重。


欢迎关注「慕课网」官方帐号,我们会一直坚持提供IT圈优质内容,分享干货知识,大家一起共同成长吧!

本文原创发布于慕课网 ,转载请注明出处,谢谢合作



标签:致命,threadB,threadA,resourceA,resourceB,并发,死锁,线程
From: https://blog.51cto.com/u_15771948/6393589

相关文章

  • .net耗时:多线程分段并发执行与单线程异步执行
    多线程执行存在线程切换的耗时,可采用单线程异步执行。性能根据实际情况调优。结合上面两种情况:可实现多线程异步执行。目前先看看下面两个例子1 多线程分段执行设备查找耗时操作///<summary>///异步查询设备///</summary>///<paramna......
  • go中的并发学习
    代码源自于https://github.com/lotusirous/go-concurrency-patterns自此对各个示例代码进行调试。1-boringpackagemainimport( "fmt" "math/rand" "time")funcboring(msgstring){ fori:=0;;i++{ fmt.Println(msg,i) time.Sleep(time.D......
  • 各个语言运行100万个并发任务需要多少内存?
    译者注:原文链接:https://pkolaczk.github.io/memory-consumption-of-async/Github项目地址:https://github.com/pkolaczk/async-runtimes-benchmarks正文在这篇博客文章中,我深入探讨了异步和多线程编程在内存消耗方面的比较,跨足了如Rust、Go、Java、C#、Python、Node.js和Elix......
  • 5.5. Java并发工具类(如CountDownLatch、CyclicBarrier等)
    5.5.1CountDownLatchCountDownLatch是一个同步辅助类,它允许一个或多个线程等待,直到其他线程完成一组操作。CountDownLatch有一个计数器,当计数器减为0时,等待的线程将被唤醒。计数器只能减少,不能增加。示例:使用CountDownLatch等待所有线程完成任务假设我们有一个任务需要三个子......
  • Java并发之原子性、可见性和有序性
    1.原子性1.1原子性的定义原子性:原子性即是一个或者多个操作,要么全程执行,并且执行的过程中不被任何因素打断,要么全部不执行。举个例子会更好理解:就像是我们去银行转账的时候,A给B转1000元,如果A的账户减少了1000之后,那么B的账户一定要增加1000。A的账户减钱,B的账户加钱,这两个操作......
  • ThreadLocal 详解【并发容器】
    ThreadLocal是什么?有哪些使用场景?ThreadLocal是一个本地线程副本变量工具类,在每个线程中都创建了一个ThreadLocalMap对象,简单说ThreadLocal就是一种以空间换时间的做法,每个线程可以访问自己内部ThreadLocalMap对象内的value。通过这种方式,避免资源在多线程间共享。原理:......
  • StampedLock:高并发场景下一种比读写锁更快的锁
    摘要:在读多写少的环境中,有没有一种比ReadWriteLock更快的锁呢?有,那就是JDK1.8中新增的StampedLock!本文分享自华为云社区《【高并发】高并发场景下一种比读写锁更快的锁》,作者:冰河。什么是StampedLock?ReadWriteLock锁允许多个线程同时读取共享变量,但是在读取共享变量的时候,不......
  • Golang扫盲式学习——GO并发 | (一)
    并发与并行......
  • Java并发(七)----线程sleep、yield、线程优先级
    1、sleep与yieldsleep调用sleep会让当前线程从Running进入TimedWaiting状态(阻塞)其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException睡眠结束后的线程未必会立刻得到执行建议用TimeUnit的sleep代替Thread......
  • gdb调试c++多线程死锁问题初步
    https://blog.csdn.net/zsiming/article/details/126695393 总结:psaux|grepfilename //找出进程IDtop-HpPID //列出进程号为PID下的所有线程gdbatattchPID //用gdb调试正在运行的进程(进程号为PID)(需要sudo权限)threadapplyallbt //这里涉及2个命令:thr......