volatile是Java中的一个关键词,我们在工作中很少会用到,但是这个是面试一定会问到的知识点,所以我们不得不去深入学习。要说volatile的使用场景,讲一个最好理解的:DCL。
关于volatile,一般面试官就一个问题:你从底层实现原理层面讲讲你对volatile的认识吧。各位要如何作答呢?
volatile很难学,为什么呢?因为想搞清楚它的实现原理,需要从以下几个方面去研究:
1、Java代码层面,即加volatile与不加有什么区别
2、Java字节码层面,即读写加了volatile修饰的变量的字节码长啥样子
3、openjdk层面,即jdk源码是如何处理加了volatile修改的变量的
4、c++层面,因为openjdk是由c/c++实现的,Java中的volatile底层用了c/c++的volatile
5、汇编层面,大家经常看到的内存屏障就是借助汇编指令lock实现的
6、编译器优化,即编译器优化到底是什么东西?优化了什么?加了volatile有什么区别……
这样讲下来大家是否对volatile有个整体上的认知了呢?为了讲明白volatile,讲得通俗易懂,目前来看至少需要3-4篇文章。本篇文章聚焦一个知识点:自上(Java代码层面)而下(c++代码)深入讲解JVM是如何处理加了volatile的读。
怎样算毕业
相信很多人都有这样的疑问:怎样才算学会了volatile?
如果你对Java中的volatile是如何实现多核多线程环境下这几个区域的数据一致性的,你就算学明白了:
1、当前CPU缓存,即当前执行写volatile修饰的变量的那个CPU,这个时候只有这个CPU缓存中的值是最新的
2、其他CPU缓存,即之前执行过volatile修饰的变量的所有CPU,这些CPU缓存中的值都是修改之前的
3、主存,即OS内存、Native Memory
4、工作内存,这是学习volatile时所有的书或文章都会提及的一个词。很多人的认知中:工作内存=虚拟机栈或者工作内存包含虚拟机栈。其实工作内存跟虚拟机栈根本就不存在任何父子关系或兄弟关系,所以你看过的很多书或文章对这块的讲解都是错误的。关于这个错误的正确认识我会在下篇讲解读volatile修饰的变量中给出答案。
5、虚拟机栈,即每个线程独有的一小块空间。
这里给出我看openjdk源码后得出的结论,具体的讲解,往后看:
读完成了工作内存与虚拟机栈的数据一致性,而且这个一致性是有延迟的,即在写之后的下一次读才能读到修改后的数值。
写完成了当前CPU缓存、其他CPU缓存、主存、工作内存这四个区域的数据一致性,那是怎么做到的呢?下篇讲。

上代码
1、Java代码
public class Test3 {public static volatile int found = 0;public static void main(String[] args) {new Thread(new Runnable() {public void run() {System.out.println("等基友送笔来...");while (0 == found) {}System.out.println("笔来了,开始写字...");}}, "我线程").start();new Thread(new Runnable() {public void run() {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("基友找到笔了,送过去...");change();}}, "基友线程").start();}public static void change() {found = 1;}}
稍微解释下这段代码:有两个线程:我线程、基友线程。『我线程』通过死循环阻塞在那里等待『基友线程』找到笔送过来,然后开始写字。『基友线程』等待一会就去找笔,找到了就送过去。
2、Java字节码(读)

这个是「我线程」run方法的字节码
3、Java字节码(写)

这是chang方法的字节码
4、openjdk源码(读)
CASE(_getstatic):{……if (cache->is_volatile()) {……if (tos_type == atos) {} else if (tos_type == itos) {SET_STACK_INT(obj->int_field_acquire(field_offset), -1);}……
#define SET_STACK_INT(value, offset) \(*((jint *)&topOfStack[-(offset)]) = (value))
5、openjdk源码(写)
CASE(_putfield):CASE(_putstatic):{……//// Now store the result//int field_offset = cache->f2_as_index();if (cache->is_volatile()) {if (tos_type == itos) {obj->release_int_field_put(field_offset, STACK_INT(-1));}……OrderAccess::storeload();
解密『读』
关于读volatile修饰的共享变量,流传着一个错误的认知:读的时候会进行判断,判断虚拟机栈中是否存在这个变量,如果有就直接用,如果没有就去主存中取值,然后在虚拟机栈中创建一份拷贝,以备后续使用。看openjdk的源码后你就会发现,这个观点纯属瞎扯。
正确的认知是:读取volatile修饰的共享变量时,总是从主存中取数据,然后压入虚拟机栈中供程序运行使用。也就是说只要你修改的volatile修饰的变量同步到主存中了,那在读的时候一定是最新的值,只不过存在很微小的一点点延迟。所以学习volatile的核心在写。
结尾
这样就将读volatile修饰的变量的本质讲清楚了,看完有疑问的童鞋留言提问。
这里给大家两个与大家认知不太一样的结论,大家可以思考下看能否想明白:
工作内存与虚拟机栈没有任何关系工作内存 = 方法区 + 堆区
关于这两个结论的正确认知及写volatile修饰的变量时是如何保证当前CPU缓存、其他CPU缓存、主存、工作内存四个区域的一致性的,下篇文章见。




