1. CPU 、主存及高速缓存的概念html
计算机的硬件组成能够抽象为由总线、IO设备、主存、处理器(CPU)等组成。其中数据存放在主存中,CPU负责指令的执行,CPU的指令执行很是快,大部分简单指令的执行只须要一个时钟周期,而一次主内存数据的读取则须要几十到几百个时钟周期,那么CPU从主存中读写数据就会有很大的延迟。这个时候就产生了高速缓存的概念。java
也就是说,当程序在运行过程当中,会将运算须要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就能够直接从它的高速缓存读取数据和向其中写入数据,当运算结束以后,再将高速缓存中的数据回写到主存当中,经过这种方式来下降CPU从主存中获取数据的延迟。大体的示意图以下:linux
图一这个模型,能够简单的认为是单核模型,在这个模型里面,以i++这个操做为例,程序执行时,会先从主内存中获取i的值,复制到高速缓存,而后CPU从高速缓存中加载并执行+1操做,操做完成后回写到高速缓存,最后再从高速缓存回写到主内存。单核模型这样操做没有任何问题,可是计算机自产生以来,一直追求的两个目标,一个是如何作的更多,另外一个就是如何计算得更快,这样带来的变化就是单核变成多核,高速缓存分级存储。大体的示意图以下:git
在图二示意图里面,i++这个操做就有问题了,由于多核CPU能够线程并行计算,在Core 0和Core 1中能够同时将i复制到各自缓存中,而后CPU各自进行计算,假设初始i为1,那么预期咱们但愿是2,可是实际因为两个CPU各自前后计算后最终主内存中的i多是2,也多是其余值。github
这个就是硬件内存架构中存在的一个问题,缓存一致性问题,就是说核1改变了变量i的值以后,核0是不知道的,存放的仍是旧值,最终对这样的一个脏数据进行操做。编程
为此,CPU的厂商定制了相关的规则来解决这样一个硬件问题,主要有以下方式:缓存
1) 总线加锁,其实很好理解总线锁,我们来看图二,前面提到了变量会从主内存复制到高速缓存,计算完成后,会再回写到主内存,而高速缓存和主内存的交互是会通过总线的。既然变量在同一时刻不能被多个CPU同时操做,会带来脏数据,那么只要在总线上阻塞其余CPU,确保同一时刻只能有一个CPU对变量进行操做,后续的CPU读写操做就不会有脏数据。总线锁的缺点也很明显,有点相似将多核操做变成单核操做,因此效率低;架构
2) 缓存锁,即缓存一致性协议,主要有MSI、MESI、MOSI等,这些协议的主要核心思想:当CPU写数据时,若是发现操做的变量是共享变量,即在其余CPU中也存在该变量的副本,会发出信号通知其余CPU将该变量的缓存行置为无效状态,所以当其余CPU须要读取这个变量时,发现本身缓存中缓存该变量的缓存行是无效的,那么它就会从内存从新读取。并发
二、 Java 内存模型oracle
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操做系统的内存访问差别,以实现让Java程序在各类平台下都能达到一致的内存访问效果。在此以前,主流程序语言(C/C++等)直接使用物理硬件和操做系统的内存模型(能够理解为相似于直接使用了硬件标准),都或多或少的在不一样的平台有着不同的执行结果。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即变量在内存中的存储和从内存中取出变量这样的底层细节。其规定了全部变量都存储在主内存,每一个线程还有本身的工做内存,线程读写变量时需先复制到工做内存,执行完计算操做后再回写到主内存,每一个线程还不能访问其余线程的工做内存。大体示意图以下:
图三咱们能够理解为和图二表达的是一个意思,工做内存能够当作是CPU高速缓存、寄存器的抽象,主内存能够当作就是物理硬件中主内存的抽象,图二这个模型会存在缓存一致性问题,图三一样也会存在缓存一致性问题。
另外,为了得到较好的执行性能,Java内存模型并无限制执行引擎使用处理器的寄存器或者高速缓存来提高指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在Java内存模型中,还会存在指令重排序的问题。
Java语言又是怎么来解决这两个问题的呢?就是经过volatile这个关键字来解决缓存一致性和指令重排问题,volatile做用就是确保可见性和禁止指令重排。
3 、volatile 背后实现
那么volatile又是怎样来确保的可见性和禁止指令重排呢?我们先来写一段单例模式代码来看看。
public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } public static void main(String[] args) { Singleton.getInstance(); } }
先看看字节码层面,JVM都作了什么。
图四
从图四能够看出,没有什么特别之处。既然在字节码层面咱们看不出什么端倪,那下面就看看将代码转换为汇编指令能看出什么端倪。转换为汇编指令,能够经过-XX:+PrintAssembly来实现,window环境具体如何操做请参考此处(https://dropzone.nfshost.com/hsdis.xht)。不过比较惋惜的是我虽然编译成功了hsdis-i386.dll(图五),放置在了JDK8下的多个bin目录,一致在报找不到这个dll文件因此我决定换个思路一窥究竟。
图五
这个思路就是去阅读openJDK的源代码。其实经过javap能够看到volatile字节码层面有个关键字ACC_VOLATILE,经过这个关键字定位到accessFlags.hpp文件,代码以下:
bool is_volatile () const { return (_flags & JVM_ACC_VOLATILE ) != 0; }
再搜索关键字is_volatile,在bytecodeInterpreter.cpp能够看到以下代码:
// // 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)); } else if (tos_type == atos) { VERIFY_OOP(STACK_OBJECT(-1)); obj->release_obj_field_put(field_offset, STACK_OBJECT(-1)); OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0); } else if (tos_type == btos) { obj->release_byte_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ltos) { obj->release_long_field_put(field_offset, STACK_LONG(-1)); } else if (tos_type == ctos) { obj->release_char_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == stos) { obj->release_short_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ftos) { obj->release_float_field_put(field_offset, STACK_FLOAT(-1)); } else { obj->release_double_field_put(field_offset, STACK_DOUBLE(-1)); } OrderAccess::storeload(); }
在这段代码中,会先判断tos_type,后面分别有不一样的基础类型的实现,好比int就调用release_int_field_put,byte就调用release_byte_field_put等等。以int类型为例,继续搜索方法release_int_field_put,在oop.hpp能够看到以下代码:
void release_int_field_put(int offset, jint contents);
这段代码实际是内联oop.inline.hpp,具体的实现是这样的:
inline void oopDesc::release_int_field_put(int offset, jint contents) { OrderAccess::release_store(int_field_addr(offset), contents); }
其实看到这,能够看到上一篇文章很熟悉的oop.hpp和oop.inline.hpp,就是很熟悉的Java对象模型。继续看OrderAccess::release_store,能够在orderAccess.hpp找到对应的实现方法:
static void release_store(volatile jint* p, jint v);
实际上这个方法的实现又有不少内联的针对不一样的CPU有不一样的实现的,在src/os_cpu目录下能够看到不一样的实现,以orderAccess_linux_x86.inline.hpp为例,是这么实现的:
inline void OrderAccess::release_store(volatile jint* p, jint v) { *p = v; }
能够看到其实Java的volatile操做,在JVM实现层面第一步是给予了C++的原语实现,接下来呢再看bytecodeInterpreter.cpp截取的代码,会再给予一个OrderAccess::storeload()操做,而这个操做执行的代码是这样的(orderAccess_linux_x86.inline.hpp):
inline void OrderAccess::storeload() { fence(); }
fence方法代码以下:
inline void OrderAccess::fence() { if (os::is_MP()) { // always use locked addl since mfence is sometimes expensive #ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); #else __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory"); #endif } }
同样能够看到和经过-XX:+PrintAssembly来看到的背后实现:lock; addl,其实这个就是内存屏障,关于内存屏障的详细说明能够看下orderAccess.hpp的注释。内存屏障提供了3个功能:确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;强制将对缓存的修改操做当即写入主存;若是是写操做,它会致使其余CPU中对应的缓存行无效。这3个功能又是怎么作到的呢?来看下内存屏障的策略:
在每一个volatile写操做前面插入storestore屏障;
在每一个volatile写操做后面插入storeload屏障;
在每一个volatile读操做后面插入loadload屏障;
在每一个volatile读操做后面插入loadstore屏障;
其中loadload和loadstore对应的是方法acquire,storestore对应的是方法release,storeload对应的是方法fence。
4 、volatile 应用场景
4.1 double check 单例
public class Singleton { private static volatile Singleton instance; private Singleton() {}; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
为何要这样写,这个网上有不少资料,这里就不赘述了。
4.2 java.util.concurrent
大量的应用在j.u.c下的各个基础类和工具栏,构成Java并发包的基础。后续并发编程的学习就能够按照这个路线图来学习了。
参考资料:
https://github.com/lingjiango/ConcurrentProgramPractice
https://stackoverflow.com/questions/4885570/what-does-volatile-mean-in-java
https://stackoverflow.com/questions/106591/do-you-ever-use-the-volatile-keyword-in-java
http://www.javashuo.com/article/p-kcqqguuv-ba.html
http://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec