Volatile
volatile案例
下面是一个刚接触volatile关键字时,关于volatile的一个demo。
主要逻辑为:先启动一个子线程t1,其中循环直至flag不为true,而主线程sleep一段时间后修改flag为true,观察到子线程是否停止。
public class VolatileDemo {
static boolean flag = true;
static long i = 0;
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t-------come in");
long startTime = System.currentTimeMillis();
while (flag) {
i++;
}
System.out.println(i);
long endTime = System.currentTimeMillis();
System.out.println("costTime:" + (endTime - startTime) + " 毫秒");
System.out.println(Thread.currentThread().getName() + "\t-------flag被设置为false,程序停止");
}, "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
// TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新flag值
flag = false;
System.out.println(Thread.currentThread().getName() + "\t 修改完成");
}
}
当flag没有用volatile修饰时,子线程可能会察觉不到主线程修改了flag。
同时,在和一些同学交流后,我们发现,子线程能否察觉flag的变动与主线程的sleep时间相关,当休眠时间为2ms时,主线程对flag的操作能及时同步到子线程中使得子线程停止,而休眠时间较长时,子线程无法察觉到flag的变动。
那么如何解释这种现象呢?
现象解释
JIT?
有同学提出这可能是因为JIT(JAVA即时编译),子线程长时间执行相同逻辑,即循环判断flag,导致JVM对其进行了优化,导致其一直察觉不到flag的变化。
我们首先来了解一下JIT技术。当JVM发现某个方法或代码块运行特别频繁的时候,会认为这是”热点代码“(Hot Spot Code)。然后JIT会把部分”热点代码“编译成本地机器相关的机器码,并进行优化,然后再把编译后的机器码缓存起来,以备下次使用。
从概念上面理解,这里JIT仅仅只是将字节码转换为机器码,避免了每次都需要将字节码转换为机器码这个过程,并没有解释为什么子线程的缓存没有再次从主线程中取数据。解释有些牵强,下面禁用JIT查看子线程能否停止。
但是,当我们禁用JIT之后,子线程依然不会停止。那么显然当前的解释是错误的。
MESI协议
MESI缓存一致性协议,是一种保证多核CPU的缓存之间的一致性协议。简单来说,如果一个变量为多个核心所有(持有该变量的线程位于多个核心上),即处于S(Share共享)状态。此时有一个线程修改了该变量的值,主存通过嗅探机制察觉到变量发生了变化,然后MESI协议会在一定的时间内通知其他线程该变量发生了变动,将当前对应的变量转变为I(Invalidated失效)状态,此时,其他线程将重新从主存中读取变量。
通过上面的描述,我们可以发现,MESI协议能够在一定的时间内通知各个相关线程,并不会出现永久无法察觉共享变量变化的情况。
while (flag) {
i++;
}
重新思考一下子线程的执行逻辑,我们可以做出如下推测:
这段代码是将flag先从内存加载到CPU cache,然后通过MOV指令将flag变量放入对应的寄存器中,因为循环中涉及到的变量比较少,flag变量此后一直存在于寄存器中,无需重新从cpu cache取数据,以至于当前线程一直认为flag是保存在寄存器中的true值,继而无法停止。(当然这个推断不一定对,仅仅是个人思考,关于重排序的一些原理还没有涉及到,不太懂)
使用Volatile
使用volatile可以保证两大特性,可见性与有序性
可见性
可见性是通过内存屏障实现的,内存屏障,又称内存栅栏,是一个CPU指令。
-
阻止屏障两边的指令重排序
-
写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
-
volatile写之前加入一个storestore屏障,禁止上面的普通写和下面的volatile写重排序,保证前面普通写的数据全部刷新到主内存中
普通写和volatile写禁止重排,volatile写和volatile写禁止重排
-
volatile写之后加入storeload屏障,禁止上面的volatile写和下面的volatile读写或普通写重排序,保证前面volatile写的操作,数据都已经全部刷新到主内存
volatile写和普通写禁止重排,volatile写和volatile读/写禁止重排
-
-
读数据时加入屏障,线程私有工作内存数据失效,重新回到主物理内存获取最新数据
显然,使用volatile之后,能够保证volatile读会重新从主存中取数据,就不会出现像上面一样的问题了。
有序性
happens-before 规则中有一条是 volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。