JavaEE初阶---多线程(三)---内存可见性/单例模式/wait,notify的使用解决线程饿死问题

文章目录

  • 1.volatile关键字
    • 1.1保证内存的可见性--引入
    • 1.2保证内存的可见性--分析
    • 1.3保证内存的可见性--解决
    • 1.4内存可见性-JMM内存模型
  • 2.notify和wait介绍
    • 2.1作用一:控制调度顺序
    • 2.2作用二:避免线程饿死
    • 2.3notify和notifyAll区分
  • 3.单例模式--经典设计模式
    • 3.1饿汉模式
    • 3.2懒汉模式
    • 3.3设计模式和线程安全
    • 3.4解决饿汉模式的安全问题
    • 3.5解决方案的优化
    • 3.6指令重排序的解决

1.volatile关键字

1.1保证内存的可见性–引入

什么叫做可见性,就是你原本应该可以看见的东西,但是现在你没有看见,这个时候,我们采用这个volatile关键字的手段,保证这个可见性一定可以被看见,不可以出现你看不见的情况,就是让你必须看见—哈哈哈哈哈,这个是不是很奇怪,通过下面的这个案例以及分析就可以明白上面的这段话的意思了;

下面的这个线程,我们使用一个全局变量控制一个死循环,这个全局变量的默认数值大小就是0,这个时候我们的这个t1线程就会一直执行,但是我们的这个t2线程里面设置一个输入,我们期望输入一个不是0的数字,可以结束这个死循环的过程,但是我们可以测试运行发现,这个是不成立的;

就是即使我们输入这个1,这个时候的t1线程也不会停止下来

image-20241022185945273

1.2保证内存的可见性–分析

这个时候上面的这个案例应该让你明白,就是我们的这个修改的过程应该是被看见,但是这个我们输入数据之后,这个线程并没有停止,就是“没看见”,因为如果看见的话,这个线程不应该继续执行的;

我们的volatile关键字就是解决这个问题的,就是我们的可见性的问题,我们先来分析一下这个为什么没有看见:

首先这个isQuit=0的处理涉及到两个步骤,一个是我们的这个load操作,就是把这个数值从我们的内存读取到这个寄存器里面去,然后是这个cmp操作,就是把我们的这个值进行比较,决定是否需要继续这个循环的过程;

这个循环的过程很快,因此这个执行的过程中就会涉及到大量的这个load和cmp的操作,因此这个时候因为刚开始执行的时候我们没有进行修改这个时候每一次load的数值都是一样的,同时介于这个从内存里面进行数据的读取很浪费时间,因此这个时候编译器进行优化,就是我们的这个load不再执行,只进行这个cmp的操作;

因此这个时候的执行过程从本应该的load cmp load cmp load cmp load cmo变化成为了这个load cmp cmp cmp cmp…因此这个编译器的优化使得我们的这个修改无法被从内存里面进行读取,因此这个时候就会出现上面的这个不可见性的问题;

1.3保证内存的可见性–解决

我们的这个volatile关键字就是用来修饰这个变量的,这个时候我们输入1的时候,上面的这个线程就必须要结束,也就是说我们的这个修改他必须看见!!!!

这个时候,你走该明白为什么叫做保证内存的可见性了吧~~

image-20241022191526049

1.4内存可见性-JMM内存模型

JMM就是java memory model简称,全程就是我们的java内存模型–这个是java的官方的规范文档上面的叫法

还是上面的这个问题,我们的官方文档上面使用这个JMM进行解释:t1线程对应的这个isQuit变量,本身是存在于这个主内存上面的,因为这个编译器的优化,这个isQuit变量就会被放到这个工作内存里面,我们在这个t2线程上面修改这个变量的时候,不会影响这个工作内存里面的内容;

其实这个官方使用的JMM解释和我们上面的这个寄存器和内存的解释逻辑是一样的,就是换了一个方式罢了,这个里面的主内存相当于是我们上面介绍的这个内存,他们说的这个工作内存相当于我们的这个cpu寄存器,理解即可,就是这个相同的逻辑使用不同的方式表达罢了;

2.notify和wait介绍

上面的两个方法都是我们的object里面的方法;

2.1作用一:控制调度顺序

image-20241023064333515

wait执行的时候需要经过三个步骤:

1.释放当前的这个锁;

2.这个线程进入阻塞的状态;

3.等待线程被唤醒,唤醒的时候重新获取锁;

监视器实际上就是我们的这个synchronized关键字修饰对象,上面的这个原因就是因为我们的这个对象还没有上锁就被解锁了,这个wait的第一步就是释放当前的这个锁,但是我们这个线程其实就没有加锁,因此这个就是监视器的状态异常,我们需要先加上这个锁,然后才可以对于这个锁进行释放(也就是使用我们的这个wait关键字);

加上这个synchronized关键字之后,,这个线程相当于就是被上锁了,这个时候我们就可以正常释放锁,并且这个线程会处于这个阻塞的状态,但是没有线程唤醒这个线程,因此这个线程就会一直处于阻塞的状态;

image-20241023093640344

下面的这个情况,我们创建两个线程,t1线程使用这个wait进行这个加锁的操作,然后使用这个wait方法的时候就是出于阻塞的状态,我们的这个t2里面使用这个notify对于这个线程进行唤醒操作,就是让这个t1线程的阻塞状态执行;

image-20241023100015767

2.2作用二:避免线程饿死

下面的这个就是线程的一个形象的图示,在这个图里面,我们的这个锁就是我们的线程里面的这个锁,我们的这个滑稽1就是正在执行的线程,我们把这个调度执行的这个情形类比成为一个取款机的情形,就是我们想要取钱,但是这个滑稽1进去之后把这个门锁上了之后,其他的四个滑稽都是处于阻塞的状态,因此这个时候只能等这个滑稽1出来,但是这个滑稽1出来之后,想在进去看看这个时候有没有钱,这个时候自己又进去了,这样的话,可能我们的这个滑稽1一直在进进出出,但是其他的四个滑稽都是没有机会进入到这个里面去的。为什么会出现这个情况;

主要是我们的滑稽1本来就是处于这个CPU上面执行的,这个时候他想要再次执行,就是很容易的,但是对于这个其他的四个线程滑稽,如果他们想要执行,就需要被这个调度,这个调度的过程需要花费一定的时间,没有我们的这个滑稽1来的方便,因此这个时候就是会出现这个滑稽1一直进进出出,但是我们的其他的连进入这个取款机里面的这个机会都没有,这个情况是很常见的;

上面的这个一个线程一直在执行,但是其他的线程没有机会被调度就是属于我们的线程饿死的情况,想要解决这个线程饿死的情况,我们可以使用这个wait和notify进行处理;

image-20241023101117781

使用这个wait之后,我们的这个线程就是按照上面说的三步操作,就是释放这个锁,然后处于阻塞的状态,具体到上面的这个例子里面,就是我们的线程1释放锁之后,处于阻塞的状态,这个时候我们其他的线程就有机会被调度,至于什么时候唤醒它,这个时候我们就可以控制了,滑稽1想要进去查看这个情况,至少需要我们的其他的滑稽都进去看了一遍之后,我们在唤醒它,这个时候就合理的解决了线程的饿死的情况,保证了线程都是会被调度的;

2.3notify和notifyAll区分

我们进行wait的线程可能是一个,其实可以是多个,这个时候,我们多个线程调用wait,都是处于阻塞的状态,这个时候,我们可以一次一次的进行唤醒,我们也可以使用这个notifyAll进行一次性全部唤醒;

3.单例模式–经典设计模式

单例:就是有的场景只需要每一个类只需要一个对象,不可以实例化多个对象;

这个情况下,可能有的人会说,我们设计程序的时候只new一次不就可以了吗,为什么会搞出来一个设计模式去处理这个问题,因为这个如果是程序员操控,可能会出现各种各样的问题,因为这个这个完全取决于我们程序员自己,有些时候如果哦我们忘记之类的,就会出现问题;

但是使用设计模式处理这个问题,就会交给这个编译器处理,如果一旦出现问题,这个机器肯定是会报错的,这个就是强制性的处理解决方案,因此这个时候就会变得更加的可靠,这个也是我们设计这个单例模式的一个原因,总之,很多事情,交给机器处理就是比交给人处理更加靠谱,这个主要是因为我们的人处理问题带有一定的不可靠性,但是我们的机器处理就是强制性的,遇到问题就是报错对于程序员进行提示,这个处理的方法更加的安全和稳妥;

3.1饿汉模式

下面的这个就是两种设计模式:饿汉模式和懒汉模式,两个模式的区别其实是很明显的,但是只听这个名字可能不是很清晰,我们集合下面的这个代码进行说明:
下面的这个就是一个实例,我们对于这个实例只允许其实例化一个对象,想要使用这个实例就是调用这个里面的方法,直接返回这个实例即可;

因为这个单例模式的主要的特点就是这个只可以实例化一个对象,因此我们把这个类的构造方法设计成为一个私有的,这样的话,我们的这个类是被封装的,里面只有一个实例,类的外面无法使用这个私有的方法,因此也就是无法进行这个对象的实例化;

但是这个饿汉模式很明显嘛,就是饿,因此这个创建实例的时间就是我们的这个类进行加载的时候就会进行这个实例的创建,这个就是饿汉模式;

image-20241023104440301

3.2懒汉模式

和上面的这个饿汉模式不一样的就是我们的这个懒汉模式,就是懒汉,因此这个时候就就不会在很早的时候进行这个类的实例化的工作,因为上面的这个饿汉模式就是在类加载的时候进行这个类的实例化;

因为上面的这个在类加载的时候就创建这个实例可能我们暂时用不到,但是这个懒汉模式就是基于这个情况,我们的懒汉模式是在使用这个实例的时候进行这个实例的创建,这样的话我们一开始的这个实例就是空的,我们用到的时候,再次使用这个new进行实例的创建;

image-20241023110048634

3.3设计模式和线程安全

上面的两个设计模式,各自都是有自己的特点的,但是两个设计模式哪一个会保证线程安全呢,这个懒汉模式其实线程就是不安全的,因为我们之前说过线程不安全的一个主要的原因就是对于这个变量进行修改;

在下面的这个饿汉模式的设计代码里面,只会涉及到去读操作,根本就谈不上修改的操作,因此这个就不会有这个线程的安全问题;

image-20241023112414407

但是在我们的这个懒汉模式里面,因为这个判断之后进行实例,这个实际上就是一个先读取,然后就会进行修改,因为原来是空的,现在是一个新的实例,这个难道不是修改吗;

但是我们的这个懒汉就是初始化,因此这个没有涉及到这个修改的内容;

下面的这个形象的展示了我们的懒汉模式出现的这个线程安全问题的情况,我们的第一个线程进行读取判断的时候,发现是空的,这个时候就会准备进行修改,但是这个时候我们的t2线程开始执行,这个是穿插执行的,因此这个时候还没有等到这个t1线程进行修改,我们的这个t2线程就会再次进行判断,因此这个时候就会t2线程先进行修改,然后这个修改之后,轮到我们的这个t1线程执行,这个时候t1线程再次修改,这个其实就是线程不安全原因

image-20241023112210074

3.4解决饿汉模式的安全问题

想要解决这个线程的不安全的问题,我们的解决方案和之前一样,就是加锁,我们需要进行这个对象的加锁,这个加锁想要解决这个问题,主要是解决这个交叉执行的问题,因此我们的这个加锁的范围就是我们的这个循环分支的范围;

这样的话,两个线程就不会出现上面的这个交叉执行的情况了;

image-20241023113531384

这样写固然可以解决这个线程安全的问题,但是一旦加上之后,我们每一次调用这个getInstance方法的时候,都需要先加上锁,但是这个安全问题只会出现在这个最开始的时候,一旦创建出来之后,我们的这个就不存在线程安全了,因为第一次是没有创建对象,但是一旦创建对象,我们的线程安全不会存在问题了,但是我们这样写就会每一次调用这个getinstance方法的时候都会加锁,这个降低了我们的程序的效率,有些画蛇添足了;

3.5解决方案的优化

下面的这个就是在原来的基础上面加上我们的这个外层的if判断,判断我们的这个实例是不是被创建,这样的话,这个只会在第一次的时候去加锁,解决了我们上面说的这个每一次都需要加锁的情况;

上面的这个加锁,其实如果我们的这个实例已经存在,就不存在线程的安全问题了,这个时候加锁就没有必要了,因此我们判断这个时候是不是进行实例的创建,如果是已经创建,我们就不会加锁了;

我们的两个if内容一致,但是意义不同,第一个是判断是否需要加锁,第二个是判断是不是需要进行这个实例的创建,因为如果没有创建实例的话,我们需要自己去创建(这个是最开始的版本就存在的);

image-20241023181740285

3.6指令重排序的解决

指令重排序也是我们的编译器优化的一个体现,这个也会对于我们的代码执行情况产生影响;

什么是指令的重排序,就是这个执行的先后顺序发生变化,这个变化也是编译器的优化导致的;

例如上面的这个new实际上就是三个步骤:

1.申请内存空间;

2.在内存空间上面使用构造方法创建对象;

3.把内存的地址,赋值给我们的instance引用;

上面的这个执行的顺序可能是这个123,但是如果出现了这个指令重排序的情况,这个的执行顺序就是132

执行13的时候,我们的这个instance已经不是一个null了,只不过这个对象没有创建,指向的是这个非法的内存区域,这个时候我们的这个t2线程进行判断,发现这个instance==null不成立,这个时候就会直接返回我们的instance实例,然后就可以对于这个实例进行操作;---------这个时候极容易出现bug!!!

image-20241023184203323

解决上面的这个问题,就是使用我们的volatile关键字修饰,因为我们之前总结过但是没有介绍过的这个volatile就有这个解决指令重排序的特性;

因此经过上面的分析,下面的这个才是我们的单例模式(懒汉式)的最终代码,这个涉及到三次调整和改进,请仔细琢磨~~

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GoogleChrome的安装和使用

Google Chrome 文章目录 Google Chrome安装主页设置扩展程序 安装 chrome官网 正规的下载好之后logo是这样的 主页设置 说明 正常情况下, GoogleChrome是无法正常访问的, 因为chrome的搜索引擎默认使用的是谷歌搜索, 而在国内是无法正常访问谷歌搜索的, 所以需要更改一下主页…

【C语言】预处理(预编译)详解(上)(C语言最终篇)

文章目录 一、预定义符号二、#define定义常量三.、#define定义宏四、带有副作用的宏参数五、宏替换的规则六、宏和函数的对比1.宏的优势2.函数的优势3.宏和函数的命名约定 一、预定义符号 学习本篇文章的内容推荐先去看前面的编译和链接,才能更好地理解和吸收&#…

基于springboot+vue的高校就业管理系统,

基于springbootvue的高校就业管理系统, 分为管理员:测试账号:10086/123 学生:测试账号:10087/123 包含个人信息、查看企业岗位信息、简历信息管理、我的应聘企业:测试账号:10070/123 包含企业信息、岗位企业信息管理、查看学生简历信息…

颠覆级AI:10秒生成超清视频

颠覆级AI:10秒生成超清视频 Pyramid-Flow 是一款开源 AI 视频生成神器💻,只需文字或图片即可极速生成高清视频🎥!高效、高清、资源需求低,适合创作广告、教学视频等多种用途🚀,快来…

VIVO售后真好:屏幕绿线,4年免费换屏

只要亮屏就有。这也太影响使用了。 本来想换趁机换手机,看了VIVO发布的X200,决定等明年的X200 ULTRA。手头这个就准备修。 查了一下价格,换屏1600,优惠1100。咸鱼上X70 PRO也就800。能不能简单维修就解决呢?于是联系…

4款免费恢复工具,一键拯救你的重要资料

不管是学习的资料、工作的文件,还是重要的照片和视频,要是丢了或者不小心删了,我们肯定急得像热锅上的蚂蚁。不过好在科技发达了,出现了一些能找回数据的神奇工具。今天,我就带你去看看四款免费数据恢复的工具&#xf…

【无人机设计与控制】改进人工势场法,引入模糊控制实现无人机路径规划和避障

摘要 本文提出了一种基于改进人工势场法并结合模糊控制的无人机路径规划和避障方法。传统的人工势场法在处理障碍物时易出现局部极小值问题,且对动态障碍物的应对能力有限。为了解决这些问题,我们引入了模糊控制来调整势场参数,从而使无人机…

Mybatis中的参数占位符:${...} 、#{...}的区别

Mybatis中的参数占位符:${...} 、#{...}的区别 在Mybatis中提供的参数占位符有两种:${…} 、#{…} #{…} 执行SQL时,会将#{…}替换为?,生成预编译SQL,会自动设置参数值使用时机:参数传递,都使…

Java面试题——微服务篇

1.微服务的拆分原则/怎么样才算一个有效拆分 单一职责原则:每个微服务应该具有单一的责任。这意味着每个服务只关注于完成一项功能,并且该功能应该是独立且完整的。最小化通信:尽量减少服务之间的通信,服务间通信越少&#xff0c…

C++11实践指北

C11:书、在线工具、库。 书 1. 《现代C语言核心特性解析》 覆盖 C11~C20 特性的讲解。 视频跟读:https://www.bilibili.com/video/BV1nN4y1j7fv 现代CPP随笔_0CCh - 每天5分钟了解现代C新特性 2. 《C Primer》第五版 基于 C11 的 C 入门书。 正在看…

Python实现贝叶斯优化器(Bayes_opt)优化简单循环神经网络分类模型(SimpleRNN分类算法)项目实战

说明:这是一个机器学习实战项目(附带数据代码文档视频讲解),如需数据代码文档视频讲解可以直接到文章最后关注获取。 1.项目背景 贝叶斯优化器 (BayesianOptimization) 是一种黑盒子优化器,用来寻找最优参数。 贝叶斯…

时间序列预测(九)——门控循环单元网络(GRU)

目录 一、GRU结构 二、GRU核心思想 1、更新门(Update Gate):决定了当前时刻隐藏状态中旧状态和新候选状态的混合比例。 2、重置门(Reset Gate):用于控制前一时刻隐藏状态对当前候选隐藏状态的影响程度。…

质量漫谈一

我知道很多同学看到这类问题,第一反应想要去寻找的就是作为测试角色,应该要如何如何去做?但是今天这里作为质量第一篇,不打算按照这样单角度去写,这类同学可以就此打住,如果在意的话,可关注后续…

python源码编译—Cython隐藏源码(windows)

文章目录 1、前言2、依赖3、操作示例 1、前言 很多时候,我们想提供我们的程序给别人使用,但又不想让别人看到我们的源代码,这样我们就需要对python代码进行编译,然后打包发送给别人使用。 2、依赖 安装Visual Studio Installer。…

uniapp移动端优惠券! 附源码!!!!

本文为常见的移动端uniapp优惠券,共有6种优惠券样式(参考了常见的优惠券),文本内容仅为示例,您可在此基础上调整为你想要的文本 预览效果 通过模拟数据,实现点击使用优惠券让其变为灰色的效果(模…

手机柔性屏全贴合视觉应用

在高科技日新月异的今天,手机柔性显示屏作为智能手机市场的新宠,以其独特的可弯曲、轻薄及高耐用性特性引领着行业潮流。然而,在利用贴合机加工这些先进显示屏的过程中,仍面临着诸多技术挑战。其中,高精度对位、应力控…

8. 数据结构—排序

目录 一、插入排序 1) 直接插入排序 优化: 折半插入排序 2)希尔排序 二、 交换排序 1)冒泡排序 2)快速排序——递归实现 三、选择排序 1)简单选择排序 2)堆排序 四、归并排序 五. 各…

论文笔记(五十一)Challenges for Monocular 6-D Object Pose Estimation in Robotics

Challenges for Monocular 6-D Object Pose Estimation in Robotics 文章概括摘要I. 介绍II. 正在进行的研究和常见数据集A. 数据集B. 正在进行的研究问题 III. 未来挑战A. 物体本体B. 可变形和关节物体C. 场景级一致性D. 基准现实性E. 环境影响F. 通用物体操控 IV. 结论 Estim…

Telephony中ITelephony的AIDL调用关系

以Android14.0源码讲解 ITelephony来自framework下的com.android.internal.telephony包下 frameworks/base/telephony/java/com/android/internal/telephony/ITelephony.aidl这个接口用于与Phone交互的界面,主要由TelephonyManager类使用,一些地方仍在…

多元线性回归【正规方程/sklearn】

多元线性回归【正规方程/sklearn】 1. 基本概念1.1 线性回归1.2 一元简单线性回归1.3 最优解1.4 多元线性回归 2. 正规方程求最优解2.1 线性回归的损失函数(最小二乘法)2.2 推导正规方程2.3 正规方程练习2.4 使用sklearn计算多元线性方程2.5 凸函数 3. 线…