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

🎇个人主页: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 控制线程执行顺序

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

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

相关文章

自动化运维管理工具----------Ansible模块详细解读

目录 一、自动化运维工具有哪些? 1.1Chef 1.2puppet 1.3Saltstack 二、Ansible介绍 2.1Ansible简介 2.2Ansible特点 2.3Ansible工作原理及流程 2.3.1内部流程 2.3.2外部流程 三、Ansible部署 3.1环境准备 3.2管理端安装 ansible 3.3Ansible相关文件 …

DirClass

DirClass 通过分析,发现当接收到DirClass远控指令后,样本将返回指定目录的目录信息,返回数据中的远控指令为0x2。 相关代码截图如下: DelDir 通过分析,发现当接收到DelDir远控指令后,样本将删除指定目录…

java入门详细教程——day01

目录 1. Java入门 1.1 Java是什么? 1.2 Java语言的历史 1.3 Java语言的分类 1.4 Java语言的特点 1.4.1 先编译再解释运行 1.4.2 跨平台 1.5 JRE和JDK(记忆) 1.6 JDK的下载和安装(应用) 1.6.1 下载 1.6.2 安…

Redis(持久化)

文章目录 1.RDB1.介绍2.RDB执行流程3.持久化配置1.Redis持久化的文件是dbfilename指定的文件2.配置基本介绍1.进入redis配置文件2.搜索dbfilename,此时的dump.rdb就是redis持久化的文件3.搜索dir,每次持久化文件,都会在启动redis的当前目录下…

智能实训-wheeltec小车-抓取(源代码)

语言 :C 源代码&#xff1a; #include <ros/ros.h> #include <image_transport/image_transport.h> #include <cv_bridge/cv_bridge.h> #include <sensor_msgs/image_encodings.h> #include <sensor_msgs/JointState.h> #include <geometry…

leetcode17. 电话号码的字母组合

题目描述&#xff1a; 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 示例 1&#xff1a; 输入&#xff1a;digits "…

产品需求文档怎么写?超详细的产品需求文档PRD模板来了!

产品需求文档怎么写&#xff1f;如何写一份简洁明了、外行人看了就能秒懂的产品需求文档呢&#xff1f;今天这篇文章&#xff0c;就来和大家分享如何编写一份高质量的产品需求文档 PRD&#xff01; 下图是来自 boardmix 模板社区的「产品需求文档」模板&#xff0c;它给出了一…

Verilog刷题笔记47

题目&#xff1a; From a 1000 Hz clock, derive a 1 Hz signal, called OneHertz, that could be used to drive an Enable signal for a set of hour/minute/second counters to create a digital wall clock. Since we want the clock to count once per second, the OneHer…

SpringBoot+Vue+Element-UI实现在线外卖系统

前言介绍 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势社会的发展和科学技术的进步&#xff0c;互联网技术越来越受欢迎。网络计算机的生活方式逐渐受到广大人民群众的喜爱&#xff0c;也逐渐进入了每个用户的…

【计算机科学速成课】笔记三——操作系统

文章目录 18.操作系统问题引出——批处理设备驱动程序多任务处理虚拟内存内存保护Unix 18.操作系统 问题引出—— Computers in the 1940s and early 50s ran one program at a time. 1940,1950 年代的电脑&#xff0c;每次只能运行一个程序 A programmer would write one at…

北京大学-知存科技存算一体联合实验室揭牌,开启知存科技产学研融合战略新升级

5月5日&#xff0c;“北京大学-知存科技存算一体技术联合实验室”在北京大学微纳电子大厦正式揭牌&#xff0c;北京大学集成电路学院院长蔡一茂、北京大学集成电路学院副院长鲁文高及学院相关负责人、知存科技创始人兼CEO王绍迪、知存科技首席科学家郭昕婕博士及企业研发相关负…

vivado Versal ACAP 可编程器件镜像 (PDI) 设置

Versal ACAP 可编程器件镜像 (PDI) 设置 下表所示 Versal ACAP 器件的器件配置设置可搭配 set_property <Setting> <Value> [current_design] Vivado 工具 Tcl 命令一起使用。 注释 &#xff1a; 在 Versal ACAP 架构上 &#xff0c; 原先支持将可编程器…

408算法题专项-2009年

题目&#xff1a; 分析&#xff1a;09年的链表题目比较简单&#xff0c;直接构建链表&#xff0c;然后根据不同思路模拟即可。 思路一&#xff1a;循环遍历 思考&#xff1a;最容易想到的思路&#xff0c;直接暴力循环。偷了一下懒&#xff0c;变量名称没用题目的&#xff0c;…

力扣每日一题105:从前序与中序序列构造二叉树

题目 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 示例 1: 输入: preorder [3,9,20,15,7], inorder [9,3,15,20,7] 输出: [3,9,20,null,null,1…

语音识别--kNN语音指令识别

⚠申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&#xff0c;若要引用&#xff0c;请标注链接地址。 全文共计3077字&#xff0c;阅读大概需要3分钟 &#x1f308;更多学习内容&#xff0c; 欢迎&#x1f44f;关注&#x1f440;【文末】我的个人微信公众号&#xf…

硬盘惊魂!文件夹无法访问怎么办?

在数字时代&#xff0c;数据的重要性不言而喻。然而&#xff0c;有时我们会遇到一个令人头疼的问题——文件夹提示无法访问。当你急需某个文件夹中的文件时&#xff0c;却被告知无法打开&#xff0c;这种感受真是难以言表。今天&#xff0c;我们就来深入探讨这个问题&#xff0…

第六代移动通信介绍、无线网络类型、白皮书

关于6G 即第六代移动通信的介绍&#xff0c; 图解通信原理与案例分析-30&#xff1a;6G-天地互联、陆海空一体、全空间覆盖的超宽带移动通信系统_6g原理-CSDN博客文章浏览阅读1.7w次&#xff0c;点赞34次&#xff0c;收藏165次。6G 即第六代移动通信&#xff0c;6G 将在5G 的基…

VTK —— 三、简单操作 - 示例3 - 将点投影到平面上(附完整源码)

代码效果 本代码编译运行均在如下链接文章生成的库执行成功&#xff0c;若无VTK库则请先参考如下链接编译vtk源码&#xff1a; VTK —— 一、Windows10下编译VTK源码&#xff0c;并用Vs2017代码测试&#xff08;附编译流程、附编译好的库、vtk测试源码&#xff09; 教程描述 本…

Day 63:单调栈 LeedCode 84.柱状图中最大的矩形

84. 柱状图中最大的矩形 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 示例 1: 输入&#xff1a;heights [2,1,5,6,2,3] 输出&#xff1a;10 解释&a…

MySQL表的增删改查

在进行表操作之前,一定要use选中数据库 注释&#xff1a;在SQL中可以使用 --空格描述 来表示注释说明 CRUD 即增加(Create)、查询(Retrieve)、更新(Update)、删除(Delete)四个单词的首字母。 文章目录 数据库约束约束类型NOT NULL约束UNIQUE&#xff1a;唯一约束DEFAULT&…