接着上一篇写,一致性的协议还有加锁的过程,要搞懂volatile关键字原理的话,需要研究它的源代码,底层是用 c语言来实现的。
一、上一篇的代码如下:
public class VolatileTest {
private static volatile boolean initFlag=false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
System.out.println("waiting data.....");
while(!initFlag){
}
System.out.println("-------------------------success");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
public void run() {
prepareData1();
}
}).start();
}
public static void prepareData1(){
System.out.println("prepareing data.....");
initFlag=true;
System.out.println("prepare data end.....");
}
}
二、查看下volatile关键字在汇编语言如何去实现:
汇编语言的指令码,看下上面的代码的汇编指令的样子
1、借助下工具:需要把下面的两个文件放入到java/jdk/jre/bin的目录下
dll文件是底层用c语言实现的,还有一个lib的包
2、在运行上面的程序的过程中:会把汇编语言给打印出来
-server -Xcomp-XX:+UnlockDiagnosticVMOptions-XX:+PrintAssembly-XX:CompileCommand=compileonly,*VolatileVisibility.prepareDate
3、在运行的时候在VM options下添加上面的参数,然后选择jre的目录。
4、运行的结果如下:下面的这一行代码时机对应的代码是在24行的initFlag=true; 赋值。
而且这个变量是用volatile来修饰的
如果不加volatile关键字的话,就不会在汇编语言这条指令下有lock指令。
5、解释下这条指令是做什么的:
在上面的add dword ptr [rsp],0h这条指令就是对工作内存赋值操作。rsp:是高速缓存的寄存器。赋值的操作是对应下面的assign的操作。把这个值赋给到工作缓存里面。
所以说assign的底层大概做的add dword ptr [rsp],0h就是这个操作
注意:这个lock指令非常的关键,就可以保证volatile关键字缓存可见性的问题。
三、Volatile缓存可见性实现原理
底层实现主要是通过汇编Lock前缀指令,它会锁定 这块内存区域的缓存(缓存行锁定)
并回写到主内存
IA-32架构软件开发者手册对lock指令的解释,下面的是Lock指令帮我们做两个事情。
1):会将当前处理器缓存行的数据立即写回到系统内存。
一旦加了volatile关键字的话一旦工作内存的值发生改变的话会马上同步到主内存,经过到总线的时候,其他的线程把自己的工作内存的值就马上失效掉。然后其他线程就会马上拿到主内存中最新的数据。
如果没有volatile关键字的话,它不一定会把工作内存的数据同步到主内存,因为改了工作内存里面的值,后面还有很多的代码在执行,
解决昨天的问题:有可能其他的线程拿到的值还是老的数据,然后请看下面第二的特征:
2):这个写回内存的操作会引在其他CPU里缓存了该内存地址的数据无效(MESI协议)
在这里会激发MESI缓存一致性协议,和开启嗅探机制,如果不加volatile关键字的话不会触发其他线程的嗅探机制和MESI缓存一致性协议,而且其他工作内存的值不会实现
所以说必须有volatile关键字,说白了,就必须有lock指令。所以说面试的时候当问到这个问题的时候不能说volatile关键字保证变量的可见性。可以结合上面的内存模型给面试官讲明白。
3):加上volatile关键字之后它还有一个特性-加锁
volatile的加锁只是对缓存行加锁,而加锁的时机是这样的:
在回写主内存之前,也就是在执行store操作之前它会对缓存行做一个操作:lock操作,直到write执行完,做一个unlock操作,这样的操作就解决了,线程1还没有执行完回显到主内存,其他线程就来读取旧值的问题。
解决:多个线程线程同时执行的问题,可以让先抢到lock锁的去回显主内存的值。
上面的过程就是volatile底层的实现原理
volatile的加锁的实现和总线加锁的区别:
1、下面的图是总线加锁的过程:总线加锁的粒度是很大的:read->load->use->assign->store->write:会经历这么多的步骤,而且如果计算的量很大的话。其他的线程需要等待很长的时间去执行。
2、而在volatile加锁的过程的粒度是很小的:store->wirte:这个步骤就是对主内存中的变量赋值而已,主内存对一个变量的赋值,每秒会支撑几十万,甚至上百万的变量来赋值,变量的赋值时间可能是0.几几毫秒的时间,可以忽略不计。所以volatile关键字能够让多线程同时的去并行执行,又解决了可见性的问题,而且效率是非常之高。
明天继续写深入了解并发编程的可见性,原子性,有序性。,谢谢大家的关注与支持!!
调试一个初次见到的代码比重写代码要困难两倍。因此,按照定义,如果你写代码非常巧妙,那么没有人足够聪明来调试它。— Brian W. Kernighan