内存可见性与指令重排序

文章目录

  • 内存可见性
    • 内存可见性问题代码演示
    • JMM(Java Memory Model)
  • 指令重排序
    • 指令重排序问题代码演示
    • 指令重排序分析
  • volatile关键字
    • volatile 保证内存可见性 & 禁止指令重排序
    • volatile 不保证原子性

在上一节介绍线程安全问题的过程中,提到了产生线程安全的原因主要有

  1. 操作系统的线程随机调度策略
  2. 对共享数据的写操作
  3. 操作不具有原子性
  4. 内存可见性问题
  5. 指令重排序问题

这五点原因中线程的随机调度是由操作系统调度模块具体实现,无法干预,而多个线程对共享数据的写操作,在某些情况下可以通过调整代码结构进行避免。操作的原子性,可以通过加锁来解决,这小节我们主要来看内存可见性和指令重排序是怎样影响到线程安全的.

由于读取内存,相比较读取寄存器是一个非常慢的操作,编编译器为了进一步提高代码执行的效率,会在保持逻辑不变的前提下,调整生产的代码内容,这样的操作在单线程环境中不会有什么问题,但是,在多线程环境下,编译器就可能会误判,内存可见性和指令重排序都是有编译器优化产生的问题

内存可见性

内存可见性问题代码演示

我们先来观察这段代码:

import java.util.Scanner;


public class Test6 {
    public static int isQuit = 0;

    // 内存可见性问题
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (isQuit == 0) {
                // 什么也不执行
            }
            System.out.println("t1 线程执行完毕");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("请输入isQuit:");
            Scanner scanner = new Scanner(System.in);
            isQuit = scanner.nextInt();
            System.out.println("t2线程执行完毕");
        });
        // 启动线程
        t1.start();
        t2.start();
    }
}

执行结果~~

请输入isQuit:
1
t2线程执行完毕

可以看到这里,输入1之后线程并没有执行完毕,那么不应该啊,isQuit的值不为0,t1线程应该会退出循环,可是并没有。我们看一张图。
在这里插入图片描述
在这个过程中,我么看看两个线程都做了什么。t1 线程在一直在读取主内存中isQuit的值,由于循环体没有执行任何逻辑,所以这个速度非常之快。t2线程先将isQuit读入工作内存,然后修改值为1后写回主内存。

如果就这样看,那么在isQuit的值被修改后t1线程也应该随之终止。但事实上Java在运行时,编译器发现在大量读取isQuit的值后,发现isQuit的值并没有改变。于是就做出来一种激进的优化(读取内存要比读取寄存器慢得多),不再读取内存,直接从寄存器中取值,这就导致了后续t2线程在我们输入值后,isQuit的值的确是改变了,但是t1线程并没有取读取内存中的isQuit,这就导致了t1线程对isQuit的内存不可见

在单线程中,编译器这样的优化一般是没有问题的,但是在并发场景下,就不得不考虑这样优化后对代码的影响。于是Java提供了volatile关键字,被这个关键字修饰后,编译器将不会进行优化。

JMM(Java Memory Model)

我们先了解一下JMM, Java虚拟机(JVM)规范文档中定义了Java内存模型.。目的是屏蔽掉各种硬件和操作系统的内存访问差异(跨平台),以实现让Java程序在各种平台下都能达到一致的并发效果。

  • 线程之间的共享变量存在 主内存 (Main Memory) - 相当于内存
  • 每一个线程都有自己的 “工作内存” (Work Memory) - 相当于寄存器
  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

指令重排序

     和内存可见性一样,指令重排序也是在一定条件下触发的编译器的”优化“,目的是提高代码效率,编译器在“保持逻辑不发生变化的情况下”,针对指令执行的顺序进行调整,这就是指令重排序。

指令重排序问题代码演示

class SingletonLazy {
    private static volatile SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() { }
}

public class Demo22 {
    public static void main(String[] args) {

    }
}

在这个单例模式(懒汉模式)中,如果是第一次创建实例,那么会涉及到一个new操作。我们简单的将new操作理解为三步:

  1. 申请内存空间
  2. 在内存空间上构造对象
  3. 把内存地址,复制给instance引用

指令重排序分析

在单线程下,先执行指令2,还是先执行指令3都可以,不影响最终的结果,但是在多线程下,就可能会出现问题。假设编译器将new操作的执行顺序优化为了 1 -> 3 -> 2,t1线程进入,创建单例,但是还没构造对象,就已经将空引用返回(锁已经释放),这是如果t2线程进入,instance还是为空此时就可能会创建出多个实例。

解决方案和内存可见性一样,使用volatile关键字,让编译器不要进行优化

volatile关键字

volatile 保证内存可见性 & 禁止指令重排序

代码在写入 volatile 修饰的变量时

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量时

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

    读取内存相较于读取寄存器来说,非常慢,使用volatile修饰虽然强制读写内存,但是保证了代码的正确性,一般来说,不会牺牲正确新来换取效率。

volatile 不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性,volatile保证的是内存可见性,禁止指令重排序。volatile只是强制cpu读取内存,但是不会保证操作的原子性(不可分割)。

不管是原子性、内存可见性还是指令重排序,都可能产生线程安全问题,我们在进行并发编程时一定要谨慎!!

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

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

相关文章

【计算思维】蓝桥杯STEMA 科技素养考试真题及解析 2

1、兰兰有一些数字卡片,从 1 到 100 的数字都有,她拿出几张数字卡片按照一定顺序摆放。想一想,第 5 张卡片应该是 A、11 B、12 C、13 D、14 答案:C 2、按照下图的规律,阴影部分应该填 A、 B、 C、 D、 答案&am…

汇编-PROC定义子过程(函数)

过程定义 过程用PROC和ENDP伪指令来声明, 并且必须为其分配一个名字(有效的标识符) 。目前为止, 我们所有编写的程序都包含了一个main过程, 例如: 当要创建的过程不是程序的启动过程时, 就用RET指令来结束它。RET强制…

Arthas 监听 Docker 部署的java项目CPU占比高的信息

1、Linux上安装Arthas wget https://alibaba.github.io/arthas/arthas-boot.jar2、docker ps 查看目标项目的容器ID 3、copy Arthas 到目标容器中 (注意有 😕 ) docker cp arthas-boot.jar d97e8666666:/4、进入到目标容器目录中 docker exec -it d97e8666666 /b…

FFmpeg 6.1 开放源码多媒体框架近日发布了重大更新

导读FFmpeg 6.1 开放源码多媒体框架近日发布了重大更新,带来了新功能、新解码器、新过滤器和许多其他变化。 在 FFmpeg 6.0 “Von Neumann “版本发布八个多月后,FFmpeg 6.1 被命名为 “Heaviside”,引入了多线程 Vulkan 硬件加速解码&#x…

【React-Router】路由快速上手

1. 创建路由开发环境 # 使用CRA创建项目 npm create-react-app react-router-pro# 安装最新的ReactRouter包 npm i react-router-dom2. 快速开始 // index.jsimport React from react; import ReactDOM from react-dom/client; import ./index.css; import App from ./App; i…

在终端输入任意的英文字符串,求最后一个单词的长度

实例要求:1、在终端输入任意的英文字符串 s,由若干单词组成,单词前后用一些空格字符隔开;2、返回字符串中 最后一个 单词的长度;3、单词 是指仅由字母组成、不包含任何空格字符的最大子字符串;示例代码&…

2023亚太杯数学建模竞赛(亚太赛)选题建议+初步分析

如下为C君的2023亚太杯数学建模竞赛&#xff08;亚太赛&#xff09;选题建议初步分析&#xff1a; 提示&#xff1a;DS C君认为的难度&#xff1a;C<A<B&#xff0c;开放度&#xff1a;A<B<C。 以下为ABC题选题建议及初步分析&#xff1a; A题&#xff1a;Image…

Day38力扣打卡

打卡记录 网格中的最小路径代价&#xff08;动态规划&#xff09; 链接 class Solution:def minPathCost(self, grid: List[List[int]], moveCost: List[List[int]]) -> int:m, n len(grid), len(grid[0])f [[0x3f3f3f3f3f] * n for _ in range(m)]f[0] grid[0]for i i…

文本分析:NLP 魔法!

一、说明 这是一个关于 NLP 和分类项目的博客。NLP 是自然语言处理&#xff0c;目前需求量很大。让我们了解如何利用 NLP。我们将通过编码来理解流程和概念。我将在本博客中介绍 BagOfWords 和 n-gram 以及朴素贝叶斯分类模型。这个博客的独特之处&#xff08;这使得它很长&…

轻量封装WebGPU渲染系统示例<38>- 动态构建WGSL材质Shader(源码)

实现原理: 基于宏定义和WGSL功能文件实现 当前示例源码github地址: https://github.com/vilyLei/voxwebgpu/blob/feature/rendering/src/voxgpu/sample/DynamicShaderBuilding.ts 当前示例运行效果: 此示例基于此渲染系统实现&#xff0c;当前示例TypeScript源码如下&#x…

【追求卓越04】数据结构--栈与队列

引导 今天我们开始学习栈与队列的内容&#xff0c;我觉得栈并不难&#xff0c;所以篇幅也就不会那么多了。在虚拟空间中&#xff0c;栈是用户空间中的一种数据结构&#xff0c;它主要用于保存局部变量。那么问题来了&#xff0c;为什么用栈来保存局部变量&#xff0c;不用别的数…

探秘开发app与小程序:一场技术与创新的博弈

app与小程序&#xff1a;一场技术与创新的博弈随着科技的飞速发展&#xff0c;移动应用程序已经成为我们日常生活中不可或缺的一部分。在这个充满竞争的时代&#xff0c;企业纷纷投身于开发各类移动应用&#xff0c;以期在市场中占据一席之地。然而&#xff0c;面对多样化的应用…

从0开始学习JavaScript--JavaScript迭代器

JavaScript迭代器&#xff08;Iterator&#xff09;是一种强大的编程工具&#xff0c;它提供了一种统一的方式来遍历不同数据结构中的元素。本文将深入探讨JavaScript迭代器的基本概念、用法&#xff0c;并通过丰富的示例代码展示其在实际应用中的灵活性和强大功能。 迭代器的…

Redis-Redis缓存高可用集群

1、Redis集群方案比较 哨兵模式 在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态&#xff0c;如果master节点异常&#xff0c;则会做主从切换&#xff0c;将某一台slave作为master&#xff0c;哨兵的配置略微复杂&#xff0c;并且性能和高可…

ubuntu cutecom串口调试工具使用方法(图形界面)

文章目录 Ubuntu下使用CuteCom进行串口调试使用指南什么是CuteCom&#xff1f;主要特点 安装CuteCom使用APT包管理器从源码编译安装 配置串口CuteCom界面解析&#xff08;启动cutecom&#xff09;使用CuteCom进行数据发送和接收配置串口参数数据接收数据发送 高级功能和技巧流控…

Pycharm的程序调试

有如下代码需要进行调试&#xff1a; i 1 while i < 10:print(i)步骤一&#xff1a;设置断点 步骤二&#xff1a;进入调试视图 方式1&#xff1a;右键单击编辑区&#xff1a;点击’Debug模块名’ ​ 方式2&#xff1a;ShiftF9 ​ 方式3&#xff1a;单机工具栏上的调试按钮…

Bean依赖注入注解开发

value Value("xfy")private String userName;private String userName;Value("xiao")public void setUserName(String userName) {this.userName userName;} Autowired // 根据类型进行注入 如果同一类型的Bean有多个&#xff0c;尝试根基名字进行二次…

地磁传感器在城市交通智能监控系统的作用

地磁传感器的功能作用 地磁传感器的功能是相当强大的&#xff1a;当驾驶员把车辆停在车位上&#xff0c;地磁传感器能自动感应车辆的到来并开始计时&#xff1b;待车辆要离开时&#xff0c;传感器会自动把停车时间传送到中继站进行计费。因此&#xff0c;解决停车收费效率低下…

Antd Design的inputNumber实现千位分隔符和小数点并存

代码来自文章: react中使用antDesign的Input/InputNumber最多保留两位小数&#xff0c;多的小数位禁止输入&#xff0c;且实现输入实时校验并添加千位分隔符, 正则忘了很多, 我主要做个笔记. //定义InputNumber的参数 const NumberProps {min: 0,//最小值max: …

druid keepAlive 导致数据库连接数飙升

一.背景 应用在执行完某个复杂业务&#xff0c;主要包含20几个查询SQL的操作后&#xff0c;会导致数据库连接池一直升高 druid版本&#xff1a;1.2.11 druid配置文件&#xff1a; spring.datasource.druid.maxActive100 spring.datasource.druid.initialSize20 spring.datas…
最新文章