07. Java线程上下文切换与死锁

1. 前言

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

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

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

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

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

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

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

3. 什么是线程死锁

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

如上图所示死锁状态,线程 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 占用的资源。

如下图所示

 

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 依次进行访问,避免了互相交叉持有等待的状态,避免了死锁的发生。

7. 小结

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

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

双网卡设置路由网络不通原因之一:静态ip设置失败

1.主要现象: 外网通,内网不通 外网IP设置 内网IP设置 路由表设置 内网不通 2.主要原因:在适配器中设置的内网静态IP没有成功 设置静态IP失败 在命令行使用ipconfig命令看到内网适配器的静态IP为192.168.0.55,并不是我们设置的1…

ubuntu 编译交叉环境arm 版本的openssl库

一,下载源码 [ Old Releases ] - /source/old/index.html 二,设置交叉编译环境 我的交叉环境是RV1126开发板,/home/rpdzkj/development/cross-compile-tools/rv1126/ 对应的是我电脑里的RV1126开发板的交叉环境下的gc g等路径存放 设置环境…

模型泛化性测试

文章目录 准备工作场景描述训练数据集获取与训练 测试结论测试方案外机进行平移外机进行旋转外机即平移又旋转该螺纹孔位置 准备工作 场景描述 场景搭建如下如所示: 在该场景中,将机器人安置在桌子左上角处(以面对显示器的视野&#xff09…

旋转机械振动信号特征提取(Python)

前缀 :将一维机械振动信号构造为训练集和测试集(Python) https://mp.weixin.qq.com/s/DTKjBo6_WAQ7bUPZEdB1TA import pandas as pd import numpy as np import scipy.io as sio import statistics_hamming from statistics_hamming import…

在质量检验中,如何才能提高生产效率

在当今这个快速发展的时代,生产效率与质量如同企业的双翼,缺一不可。然而,在追求高效率的同时,如何确保产品质量不滑坡,一直是企业面临的一大挑战。今天,我们就来分享一些成功的经验,在质量检验…

华为数通——链路聚合

链路聚合:又称为端口汇聚,是指两台交换机之间在物理上将两个或多个端口连接起来,将多条链路聚合成一条逻辑链路,从而增大链路带宽,多条物理链路之间能够相互冗余。 作用:增加链路带宽,同时提供…

精益管理咨询公司在与企业沟通时,应该如何展示自己的专业性?

在竞争激烈的商业环境中,精益管理咨询公司扮演着至关重要的角色。它们不仅为企业提供策略性的指导,还帮助企业实现资源的优化配置,从而达到提高效率、降低成本的目的。那么,精益管理咨询公司在与企业沟通时,应该如何展…

【python011】经纬度点位可视化html生成(有效方案)

1.熟悉、梳理、总结项目研发实战中的Python开发日常使用中的问题、知识点等,如获取省市等边界区域经纬度进行可视化,从而辅助判断、决策。 2.欢迎点赞、关注、批评、指正,互三走起来,小手动起来! 3.欢迎点赞、关注、批…

昇思25天学习打卡营第5天 | 网络构建

目录 1.定义模型类 2.模型层 nn.Flatten nn.Dense nn.ReLU nn.SequentialCell nn.Softmax 3.模型参数 代码实现: 总结 神经网络模型是由神经网络层和Tensor操作构成的, mindspore.nn提供了常见神经网络层的实现, 在MindSpore中&a…

从宏基因组中鉴定病毒序列(1)

Introduction 在环境微生物学和生态学研究中,宏基因组学(Metagenomics)技术的应用已经彻底改变了我们对微生物群落的理解。宏基因组学通过对环境样本中的全部遗传物质进行测序和分析,可以全面揭示微生物群落的组成、功能和相互作…

操作系统概论(二)

一、单项选择题(本大题共20小题,每小题1分,共20分) 在每小题列出的四个备选项中只有一个选项是符合题目要求的,请将其代码填写在题后的括号内。错选、多选或未选均无分。 1.操作员接口是操作系统为用户提供的使用计算机系统的手…

自产厂家将品牌入驻美国商超的详细流程及其显著优势

随着全球化的深入推进,越来越多的国内厂家开始寻求海外市场的拓展,其中美国商超成为了一个重要的目标市场。那么,国内厂家想要将产品入驻美国商超需要经历哪些详细流程呢?同时,这样的举措又有哪些显著优势呢?接下来,…

西部证券:1+1>2?

又一起券商收购拉开帷幕,证券业并购浪潮呼之欲出。 这次是——西部证券。 最近,西部证券公告称,因自身发展需要正在筹划收购国融证券控股权事项, 这是继“浙商国都”、“国联民生”、“华创太平洋”之后,今年券商并购…

HTML(16)——边距问题

清楚默认样式 很多标签都有默认的样式,往往我们不需要这些样式,就需要清楚默认样式 写法: 用通配符选择器,选择所有标签,清除所有内外边距选中所有的选择器清楚 *{ margin:0; padding:0; } 盒子模型——元素溢出 作…

Android CTS环境搭建

CTS即Compatibility Test Suite意为兼容性测试,是Google推出的Android平台兼容性测试机制。其目的是尽早发现不兼容性,并确保软件在整个开发过程中保持兼容性。只有通过CTS认证的设备才能合法的安装并使用Google market等Google应用。 搭建CTS测试环境需…

2008年 - 2021年 地级市-人口密度数据

人口密度是一个关键的人口统计指标,它反映了在一定地理范围内的人口分布情况。这个指标对于理解一个国家或地区的空间人口分布、资源分配、社会经济发展和城市规划等方面都具有重要意义。 人口密度的计算方法 人口密度是通过将一个地区的常住人口数除以其面积来计…

一文详解去噪扩散概率模型(DDPM)

节前,我们星球组织了一场算法岗技术&面试讨论会,邀请了一些互联网大厂朋友、参加社招和校招面试的同学。 针对算法岗技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备、面试常考点分享等热门话题进行了深入的讨论。 合集&#x…

恒远世达:把握现在,高考后逆袭,开启日本留学之路!

一年一度的高考已经落幕,马上就要出高考分数了,有人欢喜有人忧,奋斗学习了这么多年,就为了考上一所理想的大学,一旦没考上,心情会非常的低落。 在传统心态中,高考失利意味着人生重大失败&#…

VS Code SSH 远程连接服务器及坑点解决

背景 Linux服务器重装了一下,IP没有变化,结果VS Code再重连的时候就各种问题,导致把整个流程全部走了一遍,留个经验帖以备查看 SSH 首先确保Windows安装了ssh,通过cmd下ssh命令查看是否安装了。 没安装,…

CAD平台大模型场景显示性能优化分析总结

1.性能瓶颈原因 图元过于复杂 (1)图元内的三角形面片过多。对于CAD平台大场景,单帧三角面片数量达到5000万。 (2)图元的各种计算过多。 过多的图元。例如土建场景:将近20万的构件,绘制次数将…