内存可见性与指令重排序

文章目录

  • 内存可见性
    • 内存可见性问题代码演示
    • 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.hqwc.cn/news/208863.html

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

相关文章

IT 领域中的主要自动化趋势

48%的IT自动化流程属于IT服务管理,过去一年中,IT运维自动化增长了272%。 IT部门从交付者转变为战略伙伴 今年的《工作自动化指数》数据显示,自动化正在蔓延到组织的各个部门,越来越多的部门采用自动化,并且IT以外的员工…

shell脚本三

目录 一、循环语句 一、循环 二、for循环语句 1.列表循环 2.与c语言循环相似的for循环 3.使用for打印三角形以及乘法表 4.测试172.16.114.0网段存活的主机并将存活的主机IP地址写入文件中,未存活的主机放入另一文件中 三、while循环语句 四、until循环语句…

STM32_5(中断)

中断系统 中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行中断优先级:当…

2023年亚太地区数学建模大赛 问题B

玻璃温室中的微气候法规 温室作物的产量受到各种气候因素的影响,包括温度、湿度和风速[1]。其中,适宜的温度和风速是植物生长[2]的关键。为了调节玻璃温室内的温度、风速等气候因素,温室的设计通常采用带有温室风扇的通风系统,如…

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

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

2023亚太杯数学建模B题思路 - 玻璃温室中的微气候法规

# 1 赛题 问题B 玻璃温室中的微气候法规 温室作物的产量受到各种气候因素的影响,包括温度、湿度和风速[1]。其中,适 宜的温度和风速是植物生长[2]的关键。为了调节玻璃温室内的温度、风速等气候因素 , 温室的设计通常采用带有温室风扇的通风系统&#x…

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

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

JavaEE 多线程01

为什么引入多线程? 首先进程已经能很好的完成多任务这个情景下的并发编程了,那为什么又引入多线程呢? 这是因为在一些情景下,我么需要大量的创建和销毁进程来完成一些任务,此时多进程对系统的开销就会很大了. 假设有这样一个场景,服务器同时接收到很多个服务请求,这个时候服务…

HSV映射到圆锥坐标系

def bgr2hsvcone(img):arr_hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV)h arr_hsv[..., 0] / 180. * 2s arr_hsv[..., 1] / 255.v arr_hsv[..., 2] / 255.x np.cos(h * np.pi) * s * vy np.sin(h * np.pi) * s * vreturn np.stack((x, y, v), axis-1)

GPIO模式详解:推挽/开漏/浮空/上拉/下拉/施密特(迟滞)输入

GPIO(General Purpose Input Output)可用于执行数字输入或输出功能。典型的应用包括从/向模拟或数字传感器/设备读写数值、驱动LED、为I2C通信驱动时钟、生成外部组件的触发、发出中断等。 文章目录 1 GPIO简介2 输出模式2.1 推挽输出2.2 开漏输出 3 输入模式3.1 高阻态(浮空)、…

Pycharm的程序调试

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

某60区块链安全之Call函数簇滥用实战二学习记录

区块链安全 文章目录 区块链安全Call函数簇滥用实战二实验目的实验环境实验原理实验内容实验步骤EXP利用 Call函数簇滥用实战二 实验目的 学会使用python3的web3模块 学会并区分以太坊call、staticcall、delegatecall三种函数调用的特点 找到合约漏洞进行分析并形成利用 实验…