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 分钟前
使用python 将多个docx文件合并为一个word
开发语言·python·多个word合并为一个
不学无术の码农8 分钟前
《Effective Python》第十三章 测试与调试——使用 Mock 测试具有复杂依赖的代码
开发语言·python
tomcsdn3114 分钟前
SMTPman,smtp的端口号是多少全面解析配置
服务器·开发语言·php·smtp·邮件营销·域名邮箱·邮件服务器
EnigmaCoder19 分钟前
Java多线程:核心技术与实战指南
java·开发语言
攀小黑22 分钟前
阿里云 使用TST Token发送模板短信
java·阿里云
麦兜*28 分钟前
Spring Boot秒级冷启动方案:阿里云FC落地实战(含成本对比)
java·spring boot·后端·spring·spring cloud·系统架构·maven
自由鬼1 小时前
正向代理服务器Squid:功能、架构、部署与应用深度解析
java·运维·服务器·程序人生·安全·架构·代理
fouryears_234172 小时前
深入拆解Spring核心思想之一:IoC
java·后端·spring
codervibe2 小时前
使用 Spring Boot + JWT 实现多角色登录认证(附完整流程图)
java·后端
坚持学习永不言弃2 小时前
Ehcache、Caffeine、Memcached和Redis缓存
java