在计算机硬件结构中,为了平衡cpu和内存之间因为速度带来的差距,cpu中引入了cache做为处理器与内存之间的缓冲。在多核的处理器中,每一个核都有属于本身的cache,这就带来了cache一致性的问题。前面提到的MESI协议就是用于处理cache一致性问题的一个协议,它将cache的内容分红几个状态,并要求每一个核监听总线上传来的其余核发出的事件,根据这些外部事件以及自身操做cache的内部事件来维护cache的内容和状态,以达到cache一致性。但MESI协议中特定的优化有时会致使cache中存在临时的不一致的数据,因此引入了内存屏障来规避这个问题。java
即便有cache的存在,当处理器等待cache的载入时仍然会浪费时间。因此处理器会在当前指令因等待数据阻塞时尝试执行其余不依赖这个数据的指令,来尽量提升处理速度,这称为乱序执行。处理器会保证乱序执行的结果与顺序执行的结果一致,但仅在当前处理器范围内。若是有其余任务的计算依赖当前任务的中间结果,就有可能出现不符合预期的结果,这个问题一样能够经过内存屏障来规避。并发
java虚拟机规范中定义了java自身的内存模型,经过这个内存模型来屏蔽不一样的操做系统和硬件带来的差别,达到各个平台运行效果一致的目标。java内存模型规定全部的变量都存储在主内存中,每一个线程有本身的工做内存,线程在访问变量时都直接从工做内存中访问,而不能访问主内存。一个线程不能访问其余的线程的工做内存,线程之间的变量传递都须要通过主内存来完成。这里的线程、工做内存和主内存有有点相似计算机硬件结构中的处理器、cache和内存的关系。此外,java虚拟机中的即时编译中也有相似指令重排序的优化。性能
在java中有一个用于实现单例模式的方式,叫作“双成例检查”。双成例检查利用了synchronized和volatile关键词保证了在并发执行的状况下单例模式的正确性。可是在jdk1.5之前(不包括1.5)的版本是存在问题的,其中具体的缘由就是volatile关键词底层实如今jdk1.5才彻底正确。优化
根据volatile的特性,若是一个变量被标记为volatile,那么它将得到两个额外的属性:操作系统
在jdk1.5以前的版本,volatile并无禁止指令重排序的做用,因此即便把变量声明为volatile也会存在volatile变量先后的代码重排序的状况,这也是在jdk1.5以前不能使用双成例检查来实现单例的缘由。线程
前面提到内存屏障可以避免cache中存在过时数据以及避免乱序执行,而volatile自身也是经过内存屏障来实现上述的2个特性的。code
内存屏障一般分为几个级别:读写(保证屏障前的读写操做都早于屏障后的读写操做)、读(只保证读操做)以及写(只保证写操做)。不一样体系结构的硬件对内存屏障的实现都不同,好比在x86中内存屏障的指令是:排序
而当咱们把实际的java字节码反汇编成汇编指令时,能够看到并无这几个屏障,而是在写入volatile变量以后添加一条lock addl $0, 0 (%esp)
指令。lock指令的做用是可使当前处理器的cache内容被写入内存,同时使其余处理器的cache失效,这种操做至关于将本线程的工做内存的内容同步到主内存,也就保证了可见性。而在指令重排序的角度,因为lock指令以前的操做的结果都同步到了内存,也就至关于lock以前的操做都已经完成,这样就至关于“屏障后边的操做没法穿越到屏障前面”的效果。事件
能够看到,lock实际上具有了内存屏障的语义,那lock具体的做用是什么呢。lock是一个指令前缀,在它后面的指令会保证原子执行。其实现方式就是在指令执行期间设置处理器的LOCK#
信号,这样就能确保处理器可以互斥的操做内存(经过锁定总线来实现),当指令执行完毕以后LOCK#信号
会自动取消。从intel奔腾Pro处理器开始,当要锁定的内存地址已经被加载到cache时,会直接锁定对应的cache而不是设置LOCK#信号
。内存
也就是说,volatile的实现中经过lock前缀+一条空的指令来锁定cache,实现了可见性和禁止重排序的功能。至于为何要用addl $0, 0 (%esp)
配合lock前缀是由于lock前缀只支持内存操做类的指令,因此不能直接用lock前缀加空指令nop。