你真的懂volatile吗

写这篇文章的目的,是在交流中发现有的同窗对于volatile的happens-before规则并不太清楚,本文针对对于JMM内存模型的原子性、有序性、可见性等概念有必定了解但对于volatile理解有些模糊的同窗,大约花费7分钟左右时间缓存

由一个问题开始:ReentrantLock是如何实现与synchronized锁相同的内存可见性语义的?即:synchronized锁内操做的共享变量值修改在锁被释放后可以保证被其余线程当即看到,ReentrantLock锁可以保证吗,是如何保证的?bash

固然可以保证,不然就谈不上是同步锁,如何保证的正是本文要谈的内容微信

误区:有些同窗认为volatile修饰的共享变量写操做仅保证当前变量的内存可见性,刷新当前变量所在缓存行回内存,同时由缓存一致性协议invalid其余缓存

几个必要的名次解释

  • 原子性:在Java中,32位JVM对32位基本数据类型变量的读取和赋值操做是原子性操做,即这些操做是不可被中断的,要么所有执行,要么所有不执行(volatile另外保证了在此环境下64位long和double类型变量读取和赋值操做的原子性,注意是读取和赋值单步操做,而不包含自增这种须要被拆解为多步操做的计算)
  • 指令重排序:编译器阶段和处理器阶段的指令重排序,不存在数据依赖性的指令能够发生指令重排序,重排序可能致使程序错误,以下图,1和2之间、3和4之间均可能发生重排序,4有多步操做,处理器有可能出于优化考虑,先计算a*a,再判断if条件,而后决定是否给i赋值,若是没有同步机制,执行结果极可能是错的
class ReorderExample {  
    int a = 0;  
    boolean flag = false;  
  
    public void writer() {  
        a = 1;          // 1  
        flag = true;    // 2  
    }  
  
    public void reader() {  
        if (flag) {            // 3  
            int i = a * a; // 4  
        }  
    }  
}  
复制代码
  • cpu缓存模型 多线程

    cpu cache模型
    1)cpu寻找数据流程:L1 cache->L2 cache->L3 cache->主内存(其实L1以前还有store/load buffer,下文会有简单介绍
    2)缓存行:缓存是由缓存行组成的,通常一行缓存行有64字节,cpu存取缓存是以缓存行为最小单位操做的
    3)因为缓存结构的存在,若是没有完善的缓存一致性协议保障,就会致使多线程的内存可见性问题

  • JVM内存模型架构

    本地内存vs主内存
    1)因为线程在工做内存(即图中的本地内存)中存在变量副本(包含共享变量),而致使在没有内存同步(缓存一致性协议)的前提下,不一样线程对于共享变量操做的执行结果是不肯定的
    2)线程工做内存只是JVM的概念模型(为了适配不一样的机器结构和操做系统),JAVA线程借助了底层操做系统线程实现,一个JVM线程对应一个操做系统线程,线程的工做内存实际上是cpu寄存器和高速缓存的抽象

  • 缓存一致性协议 解决缓存不一致问题,一般来讲有如下2种方法:
    1)经过在总线加LOCK#锁的方式(效率过低
    2)经过缓存一致性协议(核心思想:当CPU写数据时,若是发现操做的变量是共享变量,且在其余CPU中也存在该变量的副本,会发出信号通知其余CPU将该变量的缓存行置为无效状态,后续当其余CPU须要读取这个变量时,发现本身缓存中缓存该变量的缓存行是无效的,就会从内存从新读取
    3)缓存一致性协议不能彻底保证内存可见性,由于为了提高性能,存在store buffer(存储缓存)、invalidate queue(失效队列)等结构,致使内存可见性受影响,由此引出了内存屏障
    4)不深究细节(细节我也不太懂),具体请搜索MESI及为何须要内存屏障app

  • 内存屏障性能

    内存屏障

    1)内存屏障:让一个CPU处理单元中的内存状态对其它处理单元可见的一项技术,阻止读写内存动做的重排序
    2)写内存屏障(Store Memory Barrier):处理器将当前store buffer(存储缓存)的值写回主存,以阻塞的方式
    3)读内存屏障(Load Memory Barrier):处理器处理invalidate queue(失效队列),以阻塞的方式
    注:上图为JMM内存屏障抽象规范,JVM会根据不一样的操做系统插入不一样的指令以达成想要的内存屏障效果
    4)LoadLoad:确保Load1所要读入的数据可以在被Load2和后续的load指令访问前读入。一般能执行预加载指令或/和支持乱序处理的处理器中须要显式声明Loadload屏障,由于在这些处理器中正在等待的加载指令可以绕过正在等待存储的指令, 而对于老是能保证处理顺序的处理器上,设置该屏障至关于无操做
    5)StoreStore:确保Store1的数据在Store2以及后续Store指令操做相关数据以前对其它处理器可见(例如向主存刷新数据)。一般状况下,若是处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它须要使用StoreStore屏障
    6)LoadStore:确保Load1的数据在Store2和后续Store指令被刷新以前读取。在等待Store指令能够越过loads指令的乱序处理器上须要使用LoadStore屏障
    7)StoreLoad Barriers:确保Store1的数据在被Load2和后续的Load指令读取以前对其余处理器可见。StoreLoad屏障能够防止一个后续的load指令不正确的使用了Store1的数据,而不是另外一个处理器在相同内存位置写入一个新数据
    8)
    内存屏障在不一样机器架构上的具体实现
    上图可见,X86仅对StoreLoad屏障作了操做,其余屏障底层实现均为no-op
    9)在x86架构下,volatile写操做会在汇编代码(可开启虚拟机-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:MaxInlineSize=0参数查看汇编代码)中插入一条StoreLoad屏障lock addl $0x0,(%rsp)

总结:多线程乱序状况汇总优化

  • 指令重排序致使的乱序,经过内存屏障禁止指令重排序解决
  • 现代处理器采用的内部缓存技术致使数据的变化不能及时反映在主存所带来的乱序,经过缓存一致性协议和内存屏障解决

为了保证多线程程序执行的正确性,JMM定义了happens-before规则,重排序须要遵照happens-before规则spa

happens-before规则操作系统

  • 程序次序规则:一段代码在单线程中执行的结果是有序的(在只有单线程执行程序的条件下,虽然可能进行了指令重排序,线程最终执行的结果与程序顺序执行的结果仍然是一致的,由于指令重排序只会对不存在数据依赖的指令进行重排序,所以,在单个线程执行条件下,程序看起来是有序执行的,但这个规则没法保证程序在多线程执行条件下的正确性
  • 监视器锁规则:对一个监视器的解锁 happens-before 于每一个后续对同一监视器的加锁(不管在单线程仍是多线程执行条件下,同一个锁若是处于被锁定的状态,必须等待持有锁线程先释放锁,其余线程才能再次竞争加锁
  • volatile变量规则:对 volatile域的写入操做 happens-before 于后续对同一 volatile的读操做(线程老是能当即读取到本线程或者其余线程对于同一个volatile变量的最新的写入值
  • 传递性:若是 A happens-before 于 B,且 B happens-before C,则 A happens-before C

volatile底层实现正是借助内存屏障和缓存一致性协议保障了happens-before规则

回到最初的问题,ReentrantLock如何实现锁的内存可见性语义?

  • ReentrantLock同步机制须要先lock()获取锁,而后进入同步代码块,最后在finally块调用unlock()方法后退出同步
  • lock()/unlock()操做均借助AbstractQueuedSynchronizer中的volatile int state变量实现
  • lock()底层调用Unsafe类的compareAndSwapInt(),该操做为原子操做,且和volatile具有相同的读写语义,所以其余线程能够当即看到state值的变化
  • unlock()时,state减1,int变量的赋值操做为原子操做,且为volatile写,所以其余线程能够当即看到state变化
  • 根据以前的happens-before规则(一、三、4条),同步块之中的内存变化也能够被其余线程当即看到,由此实现了锁的内存可见性语义
  • 同理,多线程环境下,下图代码reader()能够保障x为42
class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}
复制代码
欢迎关注个人微信公众号

68号小喇叭
相关文章
相关标签/搜索