volatile原理

171 阅读5分钟

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

volatile是Java虚拟机提供的最轻量级的同步机制

  • 直观上的作用

    • 并发可见性

    • 防止指令重排序

  • 实现原理

    • 可见性保证

      • 为什么并发存在不可见?

        • 因为普遍的CPU三级缓存结构,java定义了JMM来屏蔽底层硬件和操作系统交互的细节。

        • JMM中Java线程工作的时候分为主内存和工作内存。线程使用变量的时候都会将主内存的数据拷贝到工作内存中,操作完成后写回主内存。这就存在并发的安全问题。

        • 比如线程循环一个变量,这个变量就会一直读取工作内存的值。外部其他线程修改该值,当前线程是无法感知的。

      • 怎么实现可见的?

        • 简单说:加了volatile的变量,在写操作的时候会将数据强制刷新到内存中,并让其他线程的工作内存失效。

        • 具体实现:

          • \1. 加了volatile的变量在编译到汇编层的时候,会在volatile变量的操作指令前加上一个lock前缀。这个前缀是个汇编指令。加了该指令当该变量发生变化的时候,就会立即被写回到主存中。

          • \2. 在变量写回到主存中,数据都将是通过总线拷贝的。借助于总线嗅探的技术,当一个线程对变量修改后,加了volatile的数据会强制被刷到主内存中,这刷回的过程需要走总线,而该操作就可以被其他的工作线程感知到,然后工作内存就会将该内存置为失效。再使用的时候,就需要从主内存重新获取。

          • volatile的可见性原理其实相当于本身就是一套缓存一致性协议机制,底层来支持这套一致性协议的实现有很多,不同处理器可能不同。比较典型的就是Intel的MESI缓存一致性协议。当然在老的处理器中,还有锁总线的方式!

          • 最重要的就是这lock指令:他会让该变量的主存和缓存强制同步,也相当于一个内存屏障来保证下面的有序性。

        • 参考:www.cnblogs.com/badboys/p/1…

      • 和MESI的关系

        • 这个工作内存和主内存的工作方式其实很像 MESI。其实部分底层实现也是借助于MESI的,但是有些CPU架构中是没有MESI的,那么volatile就得通过其他的方式实现了。这也是为什么有MESI了,还需要volatile的原因之一。

        • MESI协议可以是保证cache一致性,Java为什么还需要volatile?

          • MESI只是保证了缓存一致性,重排序无法保证。

          • MESI只是intel的部分处理器有,但是volatile实现可见性是不分平台的,是抽象层的一致性。所以还是需要volatile。

          • CPU不仅仅只是三级缓存,在L1和CPU之间还可能有store buffer/invalid queue这些存储,这些可见性的保证,还需要volatile结合其他的机制来实现。

      • 总线风暴

        • 当有大量的volatile 和cas 进行数据修改的时候就会产大量嗅探消息。这样就大量占据的总线的带宽。所以不要大量使用Volatile!
    • 有序性保证

      • 为什么会乱序

        • 编译器和CPU都会对指令进行优化。不过他们都会准守as-if-serial原则(就是执行的语义不变,就像在单线程下执行一样)

        • 一旦乱序,在并发的情况下会发生比如:还如单例模式里面,因为乱序,可能单例对象还未初始化就被外部引用。img

        • 所以保证有序的主要目的就是:重要的变量在没有写完之前,不能让别人读!

      • 怎么保证不乱序

        • 简单说:加了volatile的变量,在指令生成字节码的时候,会在对应的指令序列中加入内存屏障指令。

        • 在写volatile的前加上:StoreStore屏障。(禁止前后的写操作和其重排序) 后加上StoreLoad。(禁止后面的读写和该写重排序)

        • 在读volatile变量的后面加上:LoadLoad(禁止后面的读和其重排序),LoadStore屏障(禁止后面的写和其重排序)。

      • happens-before

        • JDK5之后,提出了这个概念,定义了一系列的原则。在这些原则场景下,对一些编译器优化导致的指令重排序操作进行禁止。即A happens-before B,那么A的操作必须发生在B之前。
  • 注意事项:

    • volatile不能乱加!

      • volatile缓存的可见是针对整个缓存行cacheLine的可见。 如果volatile加的比如是一个数组,或者是一个cacheLine中会被多个线程频繁修改的变量。那么就会导致频繁的刷新缓存,就会导致效率很慢。(这就是伪共享)
    • 伪共享

      • CPU每次缓存的不是一个变量的值,而是一个cacheLine。一个cacheLine一般是 64 个字节。

      • 这个cacheLine中可能变量a和b,分别被不同的线程操作,如果a被其中一个线程修改了,那么会让其他线程的缓存全部失效。其他线程读取b的时候就需要重新刷新缓存。

      • 避免伪共享:

        • 字节填充: 一个cacheLine64字节,那么就在对应变量后面补充对应字节,比如long类型8字节,就在后面再申明7个long类型变量。

        • @ sun.misc.Contended 注解,配置后,可以自动帮我们进行缓存填充。

      • 参考:www.cnblogs.com/tong-yuan/p…

  • 总结

    • volatile保证了可见性和有序性

      • 可见性:JMM设定及缓存一致性协议(JMM的8个操作知识实现可见性的借助的操作指令,可见性保证和他们本质无关)

      • 有序性:内存屏障

    • 缺点:总线风暴,伪共享效率降低。