Java的volatile到底该如何理解?(中)

简介: Java的volatile到底该如何理解?(中)

CPU 性能优化手段 - 运行时指令重排序

编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。

重排后的指令,对于优化执行以及成熟的全局寄存器分配算法的使用,都是大有脾益的,它使得程序在计算性能上有了很大的提升。

指令重排的场景

当CPU写缓存时发现缓存区块正被其他CPU占用,为了提高CPU处理性能, 可能将后面的读缓存命令优先执行

  • 比如:
  • 13.png
  • 并非随便重排,需要遵守
  • as-if-serial语义
    不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。

编译器,runtime 和处理器都必须遵守as-if- serial语义。

也就是说:编译器和处理器不会对存在数据依赖关系的操作做重排

重排序类型

包括如下:

  • 编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。
  • 处理器可以乱序或者并行的执行指令。
  • 缓存会改变写入提交到主内存的变量的次序。

问题

CPU执行指令重排序优化下有一个问题:

虽然遵守了as-if-serial语义,单仅在单CPU自己执行的情况下能保证结果正确。

多核多线程中,指令逻辑无法分辨因果关联,可能出现乱序执行,导致程序运行结果错误。

有序性:即程序执行的顺序按照代码的先后顺序执行

使用volatile变量的第二个语义是禁止指令重排序优化

普通变量仅保证该方法执行过程所有依赖赋值结果的地方能获取到正确结果,而不保证变量赋值操作的顺序与代码执行顺序一致

因为在一个线程的方法执行过程中无法感知到这一点,这也就是JMM中描述的所谓的

线程内表现为串行的语义(Within-Thread As-If-Serial Sematics)


实例

14.png

15.png

Map configOptions;  
char[] configText;  
//此变量必须定义为volatile  
volatile boolean initialized = false;  
//假设以下代码在线程A中执行  
//模拟读取配置信息,当读取完成后  
//将initialized设置为true来通知其它线程配置可用  
configOptions = new HashMap();  
configText = readConfigFile(fileName);  
processConfigOptions(configText, configOptions);  
initialized = true;  
// 假设以下代码在线程B中执行  
// 等线程A待initialized为true,代表线程A已经把配置信息初始化完成  
while(!initialized) {  
    sleep();  
}  
//使用线程A中初始化好的配置信息  
doSomethingWithConfig();

如果定义initialized时没有使用volatile,就可能会由于指令重排序优化,导致位于线程A中最后一行的代码initialized = true被提前执行,这样在线程B中使用配置信息的代码就可能出现错误,而volatile关键字则可完美避免。

volatile变量读操作性能消耗与普通变量几乎无差,但写操作则可能稍慢,因为它需要在代码中插入许多内存屏障指令保证处理器不乱序执行。即便如此,大多数场景下volatile的总开销仍然要比锁小,在volatile与锁之中选择的唯一依据仅仅是volatile的语义能否满足使用场景的需求:

16.png

17.png

18.png

volatile修饰的变量,赋值后(前面mov %eax,0x150 (%esi) 这句便是赋值操作) 多执行了一个1ock add1 $ 0x0,(%esp),这相当于一个内存屏障(Memory Barrier/Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU 访问内存时,并不需要内存屏障

但如果有两个或更多CPU 访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了


这句指令中的add1 $0x0, (%esp)(把ESP 寄存器的值加0) 显然是一个空操作(采用这个空操作而不是空操作指令nop 是因为IA32手册规定lock前缀不允许配合nop 指令使用),关键在于lock 前缀,查询IA32 手册,它的作用是使得本CPU 的Cache写入内存,该写入动作也会引起别的CPU 或者别的内核无效化(Inivalidate) 其Cache,这种操作相当于对Cache 中的变量做了一次store和write。所以通过这样一个空操作,可让前面volatile 变量的修改对其他CPU 立即可见。

那为何说它禁止指令重排序呢?

硬件架构上,指令重排序指CPU 采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。

但并非说指令任意重排,CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。


譬如指令1把地址A中的值加10,指令2把地址A 中的值乘以2,指令3把地址B 中的值减去了,这时指令1和指令2是有依赖的,它们之间的顺序不能重排,(A+10) 2 与A2+10显然不等,但指令3 可以重排到指令i、2之前或者中间,只要保证CPU 执行后面依赖到A、B值的操作时能获取到正确的A 和B 值即可。所以在本CPU 中,重排序看起来依然是有序的。因此lock add1 $0x0,(%esp) 指令把修改同步到内存时,意味着所有之前的操作都已经执行完成,这样便形成了“指令重排序无法越过内存屏障”的效果


举个例子

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

从代码顺序上看,语句1在2前,JVM在真正执行这段代码的时候会保证**语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)

比如上面的代码中,语句1/2谁先执行对最终的程序结果并无影响,就有可能在执行过程中,语句2先执行而1后虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,**靠什么保证?数据依赖性


编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序

举例

double pi  = 3.14;    //A  
double r   = 1.0;     //B  
double area = pi * r * r; //C  

19.png

A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。

因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。

但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序

20.png

这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果

但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。这是就需要内存屏障来保证可见性了


回头看一下JMM对volatile 变量定义的特殊规则

假定T 表示一个线程,V 和W 分别表示两个volatile变量,那么在进行read, load, use,assign,store,write时需要满定如下规则


只有当线程T 对变量V 执行的前一个动作是load ,线程T 方能对变量V 执行use;并且,只有当线程T 对变量V 执行的后一个动作是use,线程T才能对变量V执行load.线程T 对变量V 的use可认为是和线程T对变量V的load,read相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值语,用于保证能看见其他线程对变量V所做的修改后的值)

只有当线程T 对变量V 执行的前一个动作是 assign ,线程T才能对变量V 执行store

并且,只有当线程T对变量V执行的后一个动作是store ,线程T才能对变量V执行assign

线程T对变量V的assign可以认为是和线程T对变量V的store,write相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V 后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改)

假定动作A 是线程T 对变量V实施的use或assign,假定动作F 是和动作A 相关联的load或store,假定动作P 是和动作F 相应的对变量V 的read 或write

类似的,假定动作B 是线程T 对变量W 实施的use或assign 动作,假定动作G是和动作B 相关联的load或store,假定动作Q 是和动作G 相应的对变量W的read或write

如果A 先于B,那么P先于Q (这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)


目录
相关文章
|
7月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
254 0
|
9月前
|
设计模式 存储 SQL
【Java并发】【volatile】适合初学者体质的volatile
当你阅读dalao的框架源码的时候,你是否会见到这样一个关键字 - - - volatie,诶,你是否会好奇,为什么要加它?加了它有什么作用?
252 14
【Java并发】【volatile】适合初学者体质的volatile
|
9月前
|
存储 缓存 安全
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是写出高端的CRUD应用。2025年,我正在沉淀自己,博客更新速度也在加快。在这里,我会分享关于Java并发编程的深入理解,尤其是volatile关键字的底层原理。 本文将带你深入了解Java内存模型(JMM),解释volatile如何通过内存屏障和缓存一致性协议确保可见性和有序性,同时探讨其局限性及优化方案。欢迎订阅专栏《在2B工作中寻求并发是否搞错了什么》,一起探索并发编程的奥秘! 关注我,点赞、收藏、评论,跟上更新节奏,让我们共同进步!
399 8
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
|
10月前
|
缓存 安全 Java
Volatile关键字与Java原子性的迷宫之旅
通过合理使用 `volatile`和原子操作,可以在提升程序性能的同时,确保程序的正确性和线程安全性。希望本文能帮助您更好地理解和应用这些并发编程中的关键概念。
290 21
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
265 0
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
342 5
Java 并发编程——volatile 关键字解析
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
196 0
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
304 7
|
缓存 安全 Java
Java volatile关键字:你真的懂了吗?
`volatile` 是 Java 中的轻量级同步机制,主要用于保证多线程环境下共享变量的可见性和防止指令重排。它确保一个线程对 `volatile` 变量的修改能立即被其他线程看到,但不能保证原子性。典型应用场景包括状态标记、双重检查锁定和安全发布对象等。`volatile` 适用于布尔型、字节型等简单类型及引用类型,不适用于 `long` 和 `double` 类型。与 `synchronized` 不同,`volatile` 不提供互斥性,因此在需要互斥的场景下不能替代 `synchronized`。
3467 3
|
缓存 Java 编译器
JAVA并发编程volatile核心原理
volatile是轻量级的并发解决方案,volatile修饰的变量,在多线程并发读写场景下,可以保证变量的可见性和有序性,具体是如何实现可见性和有序性。以及volatile缺点是什么?