×
广告

你真的懂volatile吗

96
68号小喇叭
2018.06.04 23:04* 字数 2488

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

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

当然能够保证,否则就谈不上是同步锁,如何保证的正是本文要谈的内容

误区:有些同学认为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及为什么需要内存屏障
  • 内存屏障
    内存屏障
    1)内存屏障:让一个CPU处理单元中的内存状态对其它处理单元可见的一项技术,阻止读写内存动作的重排序
    2)写内存屏障(Store Memory Barrier):处理器将当前store buffer(存储缓存)的值写回主存,以阻塞的方式
    3)读内存屏障(Load Memory Barrier):处理器处理invalidate queue(失效队列),以阻塞的方式
    4)上图为JMM内存屏障抽象规范,JVM会根据不同的操作系统插入不同的指令以达成想要的内存屏障效果
    3)LoadLoad:确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令, 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作
    4)StoreStore:确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障
    5)LoadStore:确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障
    6)StoreLoad Barriers:确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据
    7)
    内存屏障在不同机器架构上的具体实现
    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规则

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规则(1、3、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号小喇叭
后端之路
Web note ad 1