多线程教程(二十)volatile原理
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
-
对 volatile 变量的写指令后会加入写屏障
-
对 volatile 变量的读指令前会加入读屏障
volatile能够保证可见性和有序性,但是不能保证原子性。
1.如何保证可见性
首先了解下volatile中得两个屏障——写屏障和读屏障
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
简单来说,写屏障能够保证该屏障之前对共享变量的改动都不会重排序到该屏障后面,并且在有写屏障时会将缓存中的数据更新到主内存。
而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
读屏障简单点说就是所有的读取共享变量的操作不能再读屏障之前进行。
volatile实现可见性的原理
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
(缓存行无效有点复杂,需要更深入的知识,在计算机多级缓存的章节会简单介绍一下)
2.如何保证有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
读屏障和写屏障的加入能够保证被volatile修饰的变量前后的代码不会被重排序到变量的后前。
3.不能保证原子性
还是那句话,不能解决指令交错:
写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
而有序性的保证也只是保证了本线程内相关代码不被重排序
例子见下一章节double-checked locking