「JavaEE」线程安全2:内存可见性问题 wait、notify

news2024/10/7 4:25:13

🎇个人主页:Ice_Sugar_7
🎇所属专栏:JavaEE
🎇欢迎点赞收藏加关注哦!

内存可见性问题& wait、notify

  • 🍉Java 标准库的线程安全类
  • 🍉内存可见性问题
    • 🍌volatile 关键字
  • 🍉wait & notify
    • 🍌wait 和 join、sleep 的区别
  • 🍉小结

🍉Java 标准库的线程安全类

线程安全线程不安全
Vector(不推荐使用)ArrayList
HashTable(不推荐使用)LinkedList
ConcurrentHashMapHashMap
StringBufferTreeMap
StringHashSet
TreeSet
StringBuilder

这几个线程安全的类在关键的方法上加了 synchronized

不过也不是说加了 synchronized 就一定是线程安全的,关键还得看具体代码是怎么写的。就比如一个线程加锁,一个不加锁,或者两个线程给不同对象加锁,虽然都有 synchronized,但仍然存在线程安全问题


🍉内存可见性问题

先来看一个代码:

public class Main {
    public static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()-> {
            while(flag == 0) {
                
            }
            System.out.println("t1 线程结束");
        });
        Thread t2 = new Thread(()-> {
            System.out.println("请输入 flag 的值:");
            Scanner in = new Scanner(System.in);
            flag = in.nextInt();
        });
        t1.start();
        t2.start();
    }
}

在这里插入图片描述
程序运行起来后,我们会发现输入一个非 0 的数后等不到 “t1 线程已经结束” 这句话,说明 t1 线程始终在循环里面

这个 bug 和内存可见性问题有关
t1 线程中的 while 循环有两条核心指令:

  1. load 读取内存中 flag 的值到 cpu 寄存器中
  2. 拿寄存器的值和 0 进行比较(这涉及到条件跳转指令)

因为循环体是空的,所以循环执行速度会非常快,在 t2 线程执行输入之前,t1 就已经执行了上百亿次循环,而这些循环每次 load 操作的执行结果都是一样的(flag 都为 0)

频繁执行 load 指令会有很大的开销,并且每次 load 的结果都一样,那此时 JVM 就会怀疑这里的 load 操作是否真有存在的必要。所以 JVM 可能优化代码,把上面的 load 给优化掉(就相当于没有 load 这一步了,这种做法比较激进,不过确实可以提高循环的执行速度)

load 被优化之后,就不会再读取内存中 flag 的值了,而是直接使用寄存器之前缓存的值,也就是 flag == 0,所以即使后面我们通过输入改了 flag 的值,但为时已晚

这里就相当于 t2 修改了内存,但是 t1 没看到内存的变化,这就称为内存可见性问题

补充:很多代码会涉及到代码优化,JVM 会智能分析出当前写的代码哪里不太合理,然后在保证原有逻辑不变的前提下调整代码,提高程序效率。不过 “保证逻辑不变”不是一件易事,如果是单线程,那还比较好调整,而如果是多线程,那么很容易出现误判(可以视为 bug)

内存可见性问题高度依赖编译器优化,啥时候会触发这个问题,啥时候不会触发,其实不好说

🍌volatile 关键字

不过我们更希望无论代码怎么写,都不会出现这个问题,所以可以用 volatile 关键字,它可以强制关闭上述的编译器优化,这样就可以确保每次循环都会从内存中读取数据
既然是强制读取内存数据,那么开销势必会变大,效率也会因此降低,不过数据的准确性和逻辑的正确性都提高了

volatile 除了可以保证内存可见性,还可以禁止指令重排序,这个后面再讲


🍉wait & notify

我们知道,多个线程之间是随机调度的,而引入 wait 和 notify 是为了能从应用层面上干预不同线程的执行顺序。
注意这里所说的“干预”不是影响系统的线程调度策略(系统调度线程仍是无序的),而是让后执行的线程主动放弃被调度的机会,这样就能让先执行的线程把对应的代码执行完

考虑这样一个场景:有多个线程在竞争同一把锁,其中线程 t1 拿到了锁,但是它不具备执行逻辑的前提条件,也就是说它拿到锁后没法做啥
t1 释放锁之后还会和其他线程一起竞争锁,它就有可能再次拿到锁。反复获取锁但是啥都没做导致其他线程无法拿到锁
,这种情况称为线程饿死(线程饥饿)

这种问题属于概率性事件,并且发生概率还不低,因为 t1 在拿到锁时处于 RUNNABLE 状态,其他线程由于锁冲突而处于 BLOCKED 状态,需要唤醒后才能参与到锁竞争,而 t1 不用,所以 t1 在释放锁之后比较容易再次拿到锁。好在线程饿死不像死锁那样“一旦出现,程序就会挂”,但是也会极大影响其他线程运行
在这种情况下,它就应该主动放弃争夺锁(主动放弃到 cpu 上调度执行),进入阻塞状态,等到条件具备了再解除阻塞,参与竞争。这个过程简单概括就是“把机会留给有需要的人”
此时就可以使用 wait 和 notify。看 t1 是否满足当前条件,若不满足则 wait,等到有其他线程让条件满足之后,再通过 notify 唤醒 t1

wait 内部会做三件事:

  1. 释放锁
  2. 进入阻塞等待
  3. 当其他线程调用 notify 时,解除阻塞,并重新获取到锁

通过 1、2 这两步,就可以让其他线程有机会拿到锁

接下来说一下如何使用 wait
既然要释放锁,说明要先拿到锁,所以 wait 必须放在 synchronized 中使用。并且 wait 和 sleep、join 一样有可能会被 interrupt 提前唤醒,所以也要用 try-catch 语句
至于 notify,Java中特别约定要把它也放在 synchronized 里面
这两个方法都是由锁对象调用
下面拿段代码演示一下

public class Main {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker) {
                System.out.println("t1 wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 之后");
            }
        });
        Thread t2 = new Thread(()-> {
            try {
                Thread.sleep(3000);
                synchronized (locker) {
                    System.out.println("t2 notify 之前");
                    locker.notify();
                    System.out.println("t2 notify 之后");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        t1.start();
        t2.start();
    }
}

运行结果:
在这里插入图片描述
由结果来梳理一下上述代码的执行过程:

  1. t1 执行后会先拿到锁(因为 t2 sleep 5秒,这个时间够 t1 拿到锁了),并且打印第一句,执行 wait 方法后释放锁并进入阻塞状态
  2. t2 sleep 结束后顺利拿到锁并打印第二句,接着执行 notify 唤醒 t1
  3. 由于 t2 还没释放锁,所以 t1 从 WAITING 状态恢复后尝试获取锁,此时会出现一个小阻塞,这个阻塞是由锁竞争引起的
  4. t2 打印第三句之后 t2 线程执行完毕,此时 t1 可以获取到锁了,就会继续打印第四句

wait 和 notify 是通过 Object 对象联系起来的,需要同一个锁对象才能唤醒,比如下面这样是无法唤醒的

locker1.wait();
locker2.notify();

如果两个 wait 是同一个对象调用的,那 notify 会随机唤醒其中一个
如果想要一次性唤醒所有等待的线程,可以用 notifyAll。不过全唤醒后这些线程要重新获取锁,就会因为锁竞争导致它们实际上是串行执行的(谁先拿到,谁后拿到,是不确定的)

🍌wait 和 join、sleep 的区别

join 是等待另一个线程执行完才会继续执行(死等的情况下)
wait 则是等待其他线程通过 notify 通知才继续执行(也是死等的情况下),相比于 join 就不要求另一个线程必须执行完

和 join 一样,wait 也提供了带有超时时间的等待,超过超时时间没有线程来 notify 的话,就不会再等下去了

wait 和 sleep 都可以被提前唤醒。分别通过 notify 和 interrupt 唤醒
wait 主要是在不知道要等待多久的前提下使用的;而 sleep 是在知道要等多久的前提下使用的,虽然可以提前唤醒,但由于它是通过异常唤醒的,而这说明程序可能出现了一些特殊的情况,所以这种操作不应该作为正常的业务流程


🍉小结

至此,多线程的一些基础用法已经讲得差不多了,在这里总结一下学了啥

  1. 线程的基本概念、线程的特性、线程和进程的区别
  2. Thread 类创建线程
  3. Thread 类一些属性
  4. 启动线程、终止线程、等待线程
  5. 获取线程引用
  6. 线程休眠
  7. 线程状态
  8. 线程安全问题
    ①产生原因
    ②如何解决——使用 synchronized 加锁
    ③死锁问题
    ④内存可见性导致的线程安全问题——使用 volatile 保证内存可见性
  9. wait 和 notify 控制线程执行顺序

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

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

相关文章

python大数据项目中的 DIM层数据处理

一、处理维度表数据 hive的配置 -- 开启动态分区方案 -- 开启非严格模式 set hive.exec.dynamic.partition.modenonstrict; -- 开启动态分区支持(默认true) set hive.exec.dynamic.partitiontrue; -- 设置各个节点生成动态分区的最大数量: 默认为100个 (一般在生产环境中, 都…

步进电机与伺服电机的区别

什么是电机? 电机是一种将电能转换为机械能的装置,通常由定子、转子和电磁场组成。当电流通过电机的绕组时,产生的磁场会与电机中的磁场相互作用,从而使电机产生旋转运动。电机广泛应用于各种机械设备和工业生产中,是现…

5.4代码

1.本质上升序列 我想到的是用回溯去找子集一个一个判断,当然这样的话会来的很慢,然后就在网上找到了大佬的方法,这东西居然是用动态规划来的,说是最长递增子序列的类似问题 ,感觉我好像写过类似的,但是去找…

gitee关联picgo设置自己的typora_图床

一:去gitee官网创建仓库:typora_图床 1.百度搜索关键字:gitee,进入官网 2.进入gitee登录或者注册自己的账号 3.进入主页后,点击右上方 4.点击新建仓库 5.设置仓库名:typora_图床 6.点击5的创建&#xff0…

基于Springboot的校运会管理系统(有报告)。Javaee项目,springboot项目。

演示视频: 基于Springboot的校运会管理系统(有报告)。Javaee项目,springboot项目。 项目介绍: 采用M(model)V(view)C(controller)三层体系结构&a…

让我们一起来领悟带环问题的核心思想

一、带环的链表: 本质还是快慢指针来解决 关于如下一个带环链表怎么去找到他们想碰到的节点呢????我们可以想到快慢指针,第一个快点走,若是有环就会进入环,此时快指针每次走2步&am…

边缘计算含义与应用简析

边缘计算概述 边缘计算使数据存储和处理靠近生成或收集数据的位置,而不是在位于数千公里的服务器上。它将通过保持灵活性在边缘无缝可靠地部署服务。它比云计算更安全,因为不需要传输数据。因此,在将数据从边缘移动到云端时,不用…

嵌入式开发六:新建工程—寄存器版

通过前面的学习,我们对 STM32 有了个比较清晰的了解,本次我们将讲解新建寄存器库版本 MDK 工程的详细步骤。后期我们基于固件库开发,借助Keil5的环境配置助手界面RTE,不需要进行文件移植,本节的寄存器版本,…

测试通过!X-Argus、X-Gorgon、X-Medusa和X-Helions

算法分析测试 Host:api5-normal-sinfonlinea.fqnovel.com Cookie:install_id2821771262445211; ttreq1$eb27d336c987581d9ed1b36c48cab2c7bcbc7305; d_ticket38b3fb964c1b4c5955565dc0f91cfcf64df74; odin_tte25761a2638b499c0bf8840c9857e43a17899df1213ba33153a266bbddf47b5…

CSS盒子模型 (盒子模型 下)

(大家好,今天我们将继续来学习CSS的相关知识,大家可以在评论区进行互动答疑哦~加油!💕) 目录 1.4 表格的细线边框 1.5 边框会影响盒子实际大小 1.6 内边距(padding) 案例:新浪微博选框栏…

口才训练:如何用声音和语言展现自我魅力

口才训练:如何用声音和语言展现自我魅力 这里有一篇1270字左右的文章,主要介绍如何用声音和语言来展现自我魅力: 口才训练是提升个人魅力的重要途径之一。魅力不仅取决于外表,更重要的是声音和语言的运用。良好的语言表达能力可以…

CMakeLists.txt语法规则:部分常用命令说明一

一. 简介 前一篇文章简单介绍了CMakeLists.txt 简单的语法。文章如下: CMakeLists.txt 简单的语法介绍-CSDN博客 接下来对 CMakeLists.txt语法规则进行具体的学习。本文具体学习 CMakeLists.txt语法规则中常用的命令。 二. CMakeLists.txt语法规则:…

小程序账号设置以及request请求的封装

一般开发在小程序时,都会有测试版和正式版,这样在开发时会比较方便。 在开发时。产品经理都会给到测试账号和正式账号,后端给的接口也都会有测试环境用到的接口和正式环境用到的接口。 这里讲一讲我这边如何去做的。 1.在更目录随便命名一…

Ubuntu20安装torch1.13和pytorch_geometric2.3.0(对应cuda11.6)

在torch下载页面搜索1.13https://pytorch.org/get-started/previous-versions/,wheel安装方式(激活conda虚拟环境) pip install torch1.13.0cu116 torchvision0.14.0cu116 torchaudio0.13.0 --extra-index-url https://download.pytorch.org…

“100先生”逢跌加仓

原创 | 刘教链 BTC(比特币)从前日创下的局部低点56.5k持续修复至59k上方,距离重回6万刀仅有半步之遥。 群里有群友还在等25k。还有朋友发来消息问,这次是不是有机会跌回15k? 躺下睡一觉。梦里会有的。 就在前两天ETF们…

时间复杂度空间复杂度 力扣:转轮数组,消失的数字

1. 算法效率 如何衡量一个算法的好坏?一般是从时间和空间的维度来讨论复杂度,但是现在由于计算机行业发展迅速,所以现在并不怎么在乎空间复杂度了下面例子中,斐波那契看上去很简洁,但是复杂度未必如此 long long Fib…

BAPI_PR_CHANGE how to add account assignment information for service line,如何解决?

🏆本文收录于「Bug调优」专栏,主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&…

数据结构(十)----图

目录 一.图的概念 1.图的定义 2.图的类别 3.图的性质 4.几种特殊形态的图 二.图的存储结构 1.邻接矩阵(顺序存储) 2.邻接表(顺序链式存储) 3.十字链表 4.邻接多重表 四.图的遍历 1.广度优先遍历(BFS&#…

Elasticsearch 数据聚合

Bucket聚合(桶聚合) 对文档做分组,aggs 按照文档字段值或日期进行分组,能参与分词的字段不能做聚合,如text类型的字段 例如:根据城市名称做聚合,也就是城市名称对数据进行分组统计。可以加qu…

Python数据分析案例43——Fama-French回归模型资产定价(三因子/五因子)

案例背景 最近看到要做三因子模型的同学还挺多的,就是所谓的Fama-French回归模型,也就是CAMP资本资产定价模型的升级版,然后后面还升级为了五因子模型。 看起来眼花缭乱,其实抛开金融资产定价的背景,从机器学习角度来…