Volatile 作用
- 可见性
- 禁止指令重排序
1. 可见性
想要深入理解Volatile的作用,首先要理解计算机组成原理的一些知识:
多核CPU结构
存储器层级结构
L1: 寄存器
L2: 高速缓存
L3: 高速缓存
L4: 主存
L5: 磁盘
L6: 远程文件存储
CPU的多级缓存
计算机由于Cpu的运行速度远远大于数据IO的速度,因此产生了缓存的机制。每一个CPU独占L1和L2,共享L3。一个变量会被首先拷贝到各自的独立的告诉缓存中,再参与ALU的运算。
当一个变量同时参与到两个CPU进行计算时,由于L1和L2的副本只是对主存被数据的拷贝,因此不同CPU中同一个变量存在不一致的现象。CPU1和CPU2对自身内存中变量副本的更改,对方是不知道的。
Volatile的可见性的目的就是使得CPU知道当前变量在其他线程中已经被改变,本地副本失效,需要重新从主存中进行读取。
在这里引入一些其他知识, CPU在读取数据的过程中往往不是只读取一个字节的数据,而是以块的形式来读取的:
现在计算机中往往按照块的形式从内存中往高速缓存中拷贝数据, 对数据进行预热。这个数据块也被叫做缓存行,目前主流与的缓存行的大小为64字节(byte)。
依据的是程序的局部性原理
- 时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
- 空间局部性是指一旦程序访问了某个存储单元,则不久之后。其附近的存储单元也将被访问
缓存一致性协议
从上一节可以知道,在没有任何同步的情况下,多个CPU同时操作内存中的同一个变量会出现变量不一致的情况。此时人们就提出了缓存一致性协议,通过它使得多核CPU可以操作一个变量。缓存一致性协议中比较有名的是Inter的MESI协议。
缓存一致性协议有MSI,MESI,MOSI,Synapse,Firefly及DragonProtocol等等,MESI协议只是其中一种。
MESI
MESI是以缓存行(Cache Line)的几个状态来命名的(全名是Modified、Exclusive、 Share or Invalid)。该协议要求在每个缓存行上使用两个bit位来维持,使得每个数据单位可能处于M、E、S和I这四种状态之一。
状态 | 含义 | 监听 |
---|---|---|
Modified | 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 | 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。 |
Exclusive | 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 | 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。 |
Share | 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 | 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。 |
Invalid | 该Cache line无效。 |
伪共享问题(False Shared):当两个变量处于同一缓存行时,由于CPU的行读取策略,即使两个线程没有同时操作同一个变量,也会触发缓存一致性协议,从而降低效率。
此时可以通过缓存行对齐来解决这个问题,在disruptor源码中,对RingBuffer的指针定义前后,分别插入了7个 long类型变量,以保证指针所在的缓存行只有这一个操作数。(long类型变量的长度为8个字节, 7*8 = 56字节,再加上本身的长度,共占64个字节)
至此,关于Volatile的可见性问题应该都能够理解了。
注意:缓存一致性协议实际与Volatile的关系并不大,无论变量是否声明volatile,缓存一致性协议都会执行,此处提及缓存一致性协议仅是为了能更好的理解Volatile。
2. 禁止指令重排序
首先要了解什么是指令重排序,一般来说Java 代码在运行过程中,为了优化运行效率,在不影响程序结果的情况下,会对指令进行重排序,重排序可能会发生在以下两个场景里:
- Java 代码编译时
- Cpu执行指令时
如何定义不影响程序执行结果:
JMM通过happened-before原则,即指导了程序员如何编写程序又没有限制JVM在对实现优化的具体方法上做出严格的限制。
接下来,我们通过代码来一步步探索Volatile是如何做到禁止指令重排序的
Java 源码
package com.MultiThread;
public class Testvolatile {
public volatile int[] cnt = new int[2];
public static void main(String[] args) {
Testvolatile t = new Testvolatile();
System.out.println(t.cnt);
}
}
编写以上Java 类,通过javac
将其编译成class文件,然后通过javap -verbose -p Testvolatile
来查看生成的Class文件。
字节码
public class com.MultiThread.Testvolatile
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/MultiThread/Testvolatile.cnt:[I
#3 = Class #20 // com/MultiThread/Testvolatile
#4 = Methodref #3.#18 // com/MultiThread/Testvolatile."<init>":()V
#5 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Methodref #23.#24 // java/io/PrintStream.println:(Ljava/lang/Object;)V
#7 = Class #25 // java/lang/Object
#8 = Utf8 cnt
#9 = Utf8 [I
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 SourceFile
#17 = Utf8 Testvolatile.java
#18 = NameAndType #10:#11 // "<init>":()V
#19 = NameAndType #8:#9 // cnt:[I
#20 = Utf8 com/MultiThread/Testvolatile
#21 = Class #26 // java/lang/System
#22 = NameAndType #27:#28 // out:Ljava/io/PrintStream;
#23 = Class #29 // java/io/PrintStream
#24 = NameAndType #30:#31 // println:(Ljava/lang/Object;)V
#25 = Utf8 java/lang/Object
#26 = Utf8 java/lang/System
#27 = Utf8 out
#28 = Utf8 Ljava/io/PrintStream;
#29 = Utf8 java/io/PrintStream
#30 = Utf8 println
#31 = Utf8 (Ljava/lang/Object;)V
{
public volatile int[] cnt;
descriptor: [I
flags: ACC_PUBLIC, ACC_VOLATILE
public com.MultiThread.Testvolatile();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_2
6: newarray int
8: putfield #2 // Field cnt:[I
11: return
LineNumberTable:
line 3: 0
line 5: 4
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #3 // class com/MultiThread/Testvolatile
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: getfield #2 // Field cnt:[I
15: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
18: return
LineNumberTable:
line 8: 0
line 9: 8
line 10: 18
}
在字节码中可以看到
public volatile int[] cnt;
descriptor: [I
flags: ACC_PUBLIC, ACC_VOLATILE
volatile在编译过程中被翻译成了ACC_VOLATILE
.
通过对指令集查表可知:
JVM 内存屏障
继续往下,在JVM中定义了以下4种内存屏障的来解释volatile:
- LoadLoad
- LoadStroe
- StroeStore
- StroeLoad
简单来讲LoadLoad的快速记忆方法就是上一条Load指令和下一条Load指令不能互换顺序,其他三条同理。
实际实现:
-----StoreStoreBarrier------
volatile 写
----- StoreLoadBarrier-------
-----LoadLoadBarrier------
volatile 读
----- LoadStoreBarrier-------
汇编
以HotSpot为例,内存屏障最终会被转译成以下的汇编指令:
lock addl $0×0,(%rsp)
lock
: LOCK指令会使紧跟在其后面的指令变成 原子操作(atomic instruction)。暂时的锁一下总线,指令执行完了,总线就解锁了,同时lock
还有一个语义:就是释放锁之后CPU必需同步一次内存
其意义就是通过对当前的操作数做了一次+0运算,将其刷新回主存。
即volatile的最底层实现实际是通过总线锁的形式实现的,但是为什么不适用CPU级别的内存屏障呢,可能的原因是lock
的普适性更好,目前的CPU都支持总线锁吧。
CPU级内存屏障
以Inter为例存在3条CPU内存屏障指令:
①sfence:也就是save fence,写屏障指令。在sfence指令前的写操作必须在sfence指令后的写操作前完成。
②lfence:也就是load fence,读屏障指令。在lfence指令前的读操作必须在lfence指令后的读操作前完成。
③mfence:在mfence指令前得读写操作必须在mfence指令后的读写操作前完成。
参考资料:
https://b23.tv/bpNY6m
https://blog.csdn.net/u013928208/article/details/109166440
https://www.cnblogs.com/minikobe/p/12162415.html
https://blog.csdn.net/a772304419/article/details/103940426