volatile关键字

Spring Wu 552 2021-02-03

volatile关键字作用

  1. 实现共享变量的可见性。
  2. 防止指令重排序。

volatile修饰的变量是如何保证可见性的

可见性问题:

比如i++操作,在多线程环境下,i++中i的初始值为0,我们使用两个线程共同执行这个操作,理想结果是i的值为2,但是最后i的值可能为1,原因就是当线程1从主存中获取到0刷新到自己的高速缓存中。然后进行计算,在没有计算完毕时,或计算完毕还没来得及刷新到主存中时,线程2也去从主存中获取到0,然后刷新到自己的高速缓存中,然后进行计算。问题就在此时出现了。两个线程计算出来的结果变为了1。

如何解决可见性问题?

Java代码如下。

instance = new Singleton(); // instance是volatile变量

转变成汇编代码,如下。

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

需要注意的是lock xxx这段汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核CPU下会引发如下两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存。

  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

为了提高处理速度,CPU不直接与主存通信,而是先通过把主存的值刷新到自己的高速缓存中(L1,L2等),然后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量刷新到主存中。但是,就算把当前线程修改完的值刷新到主存,其他线程的值还是旧的值。再执行就会有问题,所以,在多处理器下,为了保证每个处理器中的缓存都是一致的,就会实现缓存一致性协议。每个处理器通过嗅探总线上传播的数据检查自己的数据是否过期,如果过期了。就会把自己所存在高速缓存区域的数据设置为无效状态。当处理器对这个数据进行操作时,会重新去主存中获取值。

总结lock指令:

  1. lock前缀指令会引起处理器将高速缓存中的数据刷新到主存。
  2. 某个处理器的高速缓存中的数据回写到主存,会引起其他处理器的缓存失效。

防止指令重排序

指令重排序是什么

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。但是多线程的情况下指令重排序就会给程序员带来问题。

如何防止指令重排序?

volatile关键字通过提供“内存屏障”的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

大多数的处理器都支持内存屏障的指令。

对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

在每个volatile写操作的前面插入一个StoreStore屏障。

在每个volatile写操作的后面插入一个StoreLoad屏障。

在每个volatile读操作的后面插入一个LoadLoad屏障。

在每个volatile读操作的后面插入一个LoadStore屏障。