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就有这个解决指令重排序的特性;

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

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

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

相关文章

数据库编程 SQLITE3 Linux环境

永久存储程序数据有两种方式: 用文件存储用数据库存储 对于多条记录的存储而言,采用文件时,插入、删除、查找的效率都会很差,为了提高这些操作的效率,有计算机科学家设计出了数据库存储方式 一、数据库 用来管理数据…

【Android】多渠道打包配置

目录 简介打包配置签名配置渠道配置配置打包出来的App名称正式包与测试包配置 打包方式开发工具打包命令行打包 优缺点 简介 多渠道打包 是指在打包一个 Android 应用时,一次编译生成多个 APK 文件,每个 APK 文件针对一个特定的渠道。不同的渠道可能代表…

Prompt提示词设计:如何让你的AI对话更智能?

Prompt设计:如何让你的AI对话更智能? 在人工智能的世界里,Prompt(提示词)就像是一把钥匙,能够解锁AI的潜力,让它更好地理解和响应你的需求。今天,我们就来聊聊如何通过精心设计的Pr…

厂房区域人员进出人数统计-实施方案

1.1 现状分析 传统的人流量统计方法往往依赖于人工计数或简单的视频监控系统,这些方法不仅效率低下,而且容易出错,无法满足现代仓库管理的需求。因此,我厂区决定引入先进的智能监控系统,通过集成高清摄像头、GPU服务器…

【Unity】仓库逻辑:拾取物体进仓库和扔掉物品

需求说明 目标:实现玩家移动过程中,拾取物体,物体被放入仓库;点击仓库中物体,重新扔回3D场景中逻辑。 逻辑分析: 需要玩家可以移动;需要检测玩家和物体的碰撞,并摧毁物体&#xf…

css知识点梳理2

1. 选择器拓展 在 CSS 中,可以根据选择器的类型把选择器分为基础选择器和复合选择器,复合选择器是建立在基础选择器之上,对基本选择器进行组合形成的。 ​ 复合选择器是由两个或多个基础选择器,通过不同的方式组合而成的&#xf…

【Flask】一、安装与第一个测试程序

目录 Flask简介 安装Flask 安装pip(Python包管理器) 使用pip安装Flask 验证安装 创建Flask程序 创建应用 运行 访问测试 Flask简介 Flask是一个用Python编写的轻量级Web应用框架。它被设计为易于使用和扩展,使其成为构建简单网站或复…

[项目][boost搜索引擎#4] cpp-httplib使用 | log.hpp | 前端 | 测试及总结

目录 编写http_server模块 1. 引入cpp-httplib到项目中 2. cpp-httplib的使用介绍 3. 正式编写http_server 九、添加日志到项目中 十、编写前端模块 十一. 详解传 gitee 十二、项目总结 项目的扩展 写在前面 项目 gitee 已经上传啦 (还是决定将学校和个人…

网络编程基础-Reactor线程模型-原理剖析

1、Reactor基本概念 Reactor线程模型其实是一种设计模式,其核心思想就是将输入多路复用和事件派发相结合,从而减少系统中活跃线程的数量。 像我们之前讲到的文章网络编程基础-IO模型深入理解_网络io-CSDN博客提到了其中网络IO模型(BIO、NIO…

asp.net core 入口 验证token,但有的接口要跳过验证

asp.net core 入口 验证token,但有的接口要跳过验证 在ASP.NET Core中,你可以使用中间件来验证token,并为特定的接口创建一个属性来标记是否跳过验证。以下是一个简化的例子: 创建一个自定义属性来标记是否跳过验证: public clas…

基于PHP的http字段查询与注册(V1)(持续迭代)

目录 版本说明: 实现环境(WAMP): 数据库链接 查询页面 php处理逻辑 字段添加 版本说明: 该查询功能以查询http首部字段为目的实现的字段属性、字段内容的查询,以及对新字段信息的数据注册。 v1实现…

python 制作 发货单 (生成 html, pdf)

起因, 目的: 某个小店,想做个发货单。 过程: 先写一个 html 模板。准备数据, 一般是从数据库读取,也可以是 json 格式,或是 python 字典。总之,是数据内容。使用 jinja2 来渲染模板。最终的结果可以是 h…

多线程进阶——线程池的实现

什么是池化技术 池化技术是一种资源管理策略,它通过重复利用已存在的资源来减少资源的消耗,从而提高系统的性能和效率。在计算机编程中,池化技术通常用于管理线程、连接、数据库连接等资源。 我们会将可能使用的资源预先创建好,…

WPF+MVVM案例实战(七)- 系统初始化界面字体描边效果实现

文章目录 1、案例效果展示2、项目准备3、功能实现1、资源获取2、界面代码3、后台代码 4 源代码获取 1、案例效果展示 2、项目准备 打开项目 Wpf_Examples,新建系统初始化界面 WelcomeWindow.xmal,如下所示: 3、功能实现 1、资源获取 案例中使用的CSD…

Java | Leetcode Java题解之第516题最长回文子序列

题目&#xff1a; 题解&#xff1a; class Solution {public int longestPalindromeSubseq(String s) {int n s.length();int[][] dp new int[n][n];for (int i n - 1; i > 0; i--) {dp[i][i] 1;char c1 s.charAt(i);for (int j i 1; j < n; j) {char c2 s.char…

【Java并发编程】信号量Semaphore详解

一、简介 Semaphore&#xff08;信号量&#xff09;&#xff1a;是用来控制同时访问特定资源的线程数量&#xff0c;它通过协调各个线程&#xff0c;以保证合理的使用公共资源。 Semaphore 一般用于流量的控制&#xff0c;特别是公共资源有限的应用场景。例如数据库的连接&am…

Python | Leetcode Python题解之第516题最长回文子序列

题目&#xff1a; 题解&#xff1a; class Solution:def longestPalindromeSubseq(self, s: str) -> int:n len(s)dp [[0] * n for _ in range(n)]for i in range(n - 1, -1, -1):dp[i][i] 1for j in range(i 1, n):if s[i] s[j]:dp[i][j] dp[i 1][j - 1] 2else:dp…

从病理AI的基础模型发展历程,看未来的医学AI发展趋势|个人观点·24-10-23

小罗碎碎念 在临床相关的人工智能&#xff08;AI&#xff09;模型发展方面&#xff0c;传统上需要大量标注数据集&#xff0c;这使得AI的进步主要围绕大型中心和私营企业展开。所以&#xff0c;在这期推文中&#xff0c;我会介绍一些已经商用的模型&#xff0c;并且为计划进军…

逻辑推理学习笔记

目的 立场辩护整理思绪 基本框架 论题 &#xff08;变化&#xff09; 我要证明&#xff08;讨论对象 变化&#xff09; 论据 &#xff08;变化&#xff09; 拿什么证明&#xff1f;也就是证据呈现。 论证 &#xff08;不变&#xff09; 要如何证明&#xff1f;逻辑框架…

通过conda install -c nvidia cuda=“11.3.0“ 安装低版本的cuda,但是却安装了高版本的12.4.0

问题 直接通过 conda install -c nvidia cuda"11.3.0"安装得到的却是高版本的 不清楚原理 解决方法 不过我们可以分个安装 runtime toolkit 和 nvcc 安装指定版本的 cudatoolkit 和 nvcc conda install -c nvidia cuda-cudart"11.3.58" conda instal…