volatile关键字及内存可见性,指令重排序

volatile关键字

用法

修饰需要多个线程共享的变量,例如静态变量或实例变量(多个线程共享该实例变量,可能同时修改和读取它)

作用

  1. 保证内存可见性
  2. 防止指令重排序

验证

程序验证

可见性验证

程序示例如下:

java 复制代码
package com.jvm;
public class TestVolatile {

	private static boolean stop = false;
	
	public static void main(String[] args) throws InterruptedException {
		Thread thread1 = new Thread(() -> {
			while (!stop) {
//				try {
//					Thread.sleep(1000);
//				}
//				catch (InterruptedException e) {
//					e.printStackTrace();
//				}
			}
			System.out.println("loop end");
		});

		Thread thread2 = new Thread(() -> {
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			stop = true;
		});


		thread1.start();
		thread2.start();
	}
}

上面的程序有两个线程在访问静态变量stop,程序输出结果是:一直处于循环中,并且永远不会输出loop end字符串,因为线程2修改了stop值,但是线程1不会知道,因为线程2修改的只是它的缓存中的值,而线程1读取的值也只是它的缓存中的值。具体解释参考下面内容。

如果将stop前面加上volatile,则程序会退出循环,并输出loop end字符串,这正是volatile保证内存可见性的作用。

有个小插曲,写这段程序测试时,刚开始线程1中Thread.sleep方法没有被注释,想着只是为了循环慢一点,但是当运行时,发现当线程1输出了几次loop字符串时,意外输出了loop end字符串,经过多次调试与查资料,Thread.sleep可能会导致线程1观察到修改后的stop变量的值,具体不深究了。
2.

汇编指令验证

通过安装hsdis工具,并配置下面的VM参数,则可以在程序运行后查看到程序对应的汇编指令:

java 复制代码
-XX:+UnlockDiagnosticVMOptions
// 打印汇编代码
-XX:+PrintAssembly
-Xcomp
-XX:+LogCompilation
-XX:LogFile=D:\jvmcompilelog\hotspot.log
// 代表只编译TestVolatile类的change方法
-XX:CompileCommand=compileonly,*TestVolatile.change

change方法是一个修改被volatile修饰的变量的值的方法,如下:

java 复制代码
private static void change() {
	stop = true;
}

编译后输出的汇编指令如下:

java 复制代码
  0x000001f3eb4701ea: movabs $0x720b25180,%rsi  ;   {oop(a 'java/lang/Class'{0x0000000720b25180} = 'com/jvm/TestVolatile')}
  0x000001f3eb4701f4: mov    $0x1,%edi
  0x000001f3eb4701f9: mov    %dil,0x88(%rsi)
  0x000001f3eb470200: lock addl $0x0,-0x40(%rsp)  ;*putstatic stop {reexecute=0 rethrow=0 return_oop=0}
                                                ; - com.jvm.TestVolatile::change@1 (line 99)

其中lock addl $0x0,-0x40(%rsp)正是volatile汇编后的关键指令,这个指令中addl $0x0,-0x40(%rsp)是将rsp寄存器的值和0相加,lock是一个指令前缀,被它修饰的操作在执行时会独占共享内存。

lock前缀指令有两个很重要的作用:

  • 当执行该指令时,会把CPU缓存中的数据刷新到主内存,并使其他CPU缓存中缓存的该数据失效
  • 防止该指令的前后指令进行重排序,也就类似内存屏障的作用

正因为它的这两个作用,所以当volatile修饰的变量被修改后,其他线程能立即观察到修改后的值,因为其他线程中缓存的该变量的值失效了,必须从主存中重新加载。

指令重排序

定义

CPU会根据性能需要对指令进行乱序执行,但是在单线程内保证按照as-if-serial语义执行,其实就是保证乱序执行后的结果和单线程中程序定义的顺序执行结果一致。

问题

乱序执行在单线程中能保证程序正常执行,但是在多线程中不能保证,所以需要使用内存屏障来保证特定顺序。

内存屏障

定义

内存屏障主要是一种指令,根据类型的不同,实现不同的效果,用来防止该指令前后的指令进行重排序和刷新CPU缓存数据到主存。

分类

汇编语言

  • sfence
    store fence:写屏障,保证写屏障之前的写入操作可见性先于写屏障之后的写入操作的可见性。
  • lfence
    load fence: 读屏障,保证读屏障之前的加载操作先于读屏障之后的加载操作,并且可以保证读屏障之前的加载操作序列化执行。
  • mfence
    memory fence: 内存屏障,保证内存屏障之前的读写操作可见性先于内存屏障之后的读或写操作,并且保证内存屏障之前的读写操作序列化执行。

JVM层面

JSR定义了四种内存屏障来屏蔽不同硬件平台的内存屏障实现。

  • LoadLoad:防止两个读取操作重排序。
  • LoadStore:防止读和写操作重排序。
  • StoreStore:防止两个写操作重排序。加入指令1是将x的值设置为1,指令2是将y的值设置为true,一个线程监控y的值,当为true时,读取x的值,如果指令1和2重排序,会导致线程读取到x的值不为1。
  • StoreLoad:防止写和读重排序,并且保证写入的数据刷新到主存。

Happened-Before关系

作用

JMM用来描述内存可见性的高级概念。满足该关系的两个操作能保证内存可见性。

该关系有几个原则,每种原则描述的都是该关系之前的操作对于该关系之后的操作可见。

并且该关系具有传递性,如果A 先于 B,B 先于 C,则A 先于 C,那么A的操作结果对C可见。

验证

相关推荐
微风中的麦穗5 小时前
【MATLAB】MATLAB R2025a 详细下载安装图文指南:下一代科学计算与工程仿真平台
开发语言·matlab·开发工具·工程仿真·matlab r2025a·matlab r2025·科学计算与工程仿真
2601_949146535 小时前
C语言语音通知API示例代码:基于标准C的语音接口开发与底层调用实践
c语言·开发语言
开源技术5 小时前
Python Pillow 优化,打开和保存速度最快提高14倍
开发语言·python·pillow
学嵌入式的小杨同学5 小时前
从零打造 Linux 终端 MP3 播放器!用 C 语言实现音乐自由
linux·c语言·开发语言·前端·vscode·ci/cd·vim
毕设源码-朱学姐6 小时前
【开题答辩全过程】以 基于JavaWeb的网上家具商城设计与实现为例,包含答辩的问题和答案
java
mftang7 小时前
Python 字符串拼接成字节详解
开发语言·python
jasligea7 小时前
构建个人智能助手
开发语言·python·自然语言处理
kokunka7 小时前
【源码+注释】纯C++小游戏开发之射击小球游戏
开发语言·c++·游戏
C雨后彩虹7 小时前
CAS与其他并发方案的对比及面试常见问题
java·面试·cas·同步·异步·
云栖梦泽8 小时前
易语言开发从入门到精通:补充篇·网络编程进阶+实用爬虫开发·API集成·代理IP配置·异步请求·防封禁优化
开发语言