zoukankan      html  css  js  c++  java
  • volatile原理小结

    Volatile原理小结

    参考《java多线程编程实战指南(核心篇)》

    背景—基本内存屏障

    需要提前了解MESI协议。
    处理器最终能保障共享变量的可见性,但是由于写缓冲器和无效化队列等硬件的特性,这种保障可能并不及时,所以为了保障及时的同步,需要借助内存屏障。
    处理器支持哪种重排序就会提供禁止相应重排序的指令,这些指令被称为基本内存屏障--LoadLoad\LoadStore\StoreLoad\StoreStore。
    这类指令的作用是禁止该指令左侧的任何X操作和该指令右侧的任何Y操作之间的重排序。
    LoadLoad是通过清空无效化队列来禁止重排序的。使处理器有机会将其他处理器对共享变量所做的更新同步到该处理器的高速缓存中,后面的Load不会读到脏数据。
    StoreStore是将前面阶段写操作在写缓冲器中的条目打上标签,后面阶段的缓存条目一定要在标签条目之后进入高速缓存,即缓存条目即使为E或者M但是写缓冲器中如果有标签条目,那么这些缓存条目也要进入写缓冲器排队,不能直接进入高速缓存。
    StoreLoad屏障一般实现为一个通用的内存屏障,即StoreLoad可以实现其他三个屏障的作用,可以替代其他的屏障,但是它的开销也是最大的,因为会清空无效化队列,同时将写缓冲器中的条目冲刷到高速缓存。StoreLoad屏障既可以将其他处理器对共享变量所做的更新同步到该处理器的高速缓存(清空无效化队列),也可以使其执行处理器对共享变量所做的更新对其他处理器是可同步的(写缓冲器条目冲刷)。

    背景—内存屏障

    内存屏障是对跨处理器架构的针对内存读写操作指令比较底层的抽象。

    • 获取屏障
      获取屏障的使用方式是在一个读操作之后插入该内存屏障,作用是禁止该读操作与其后的任何读写操作之间的重排序,这相当于在进行后续操作之前先要获得相应共享数据的所有权。相当于基本内存屏障的LoadLoad+LoadStore组合
    • 释放屏障
      释放屏障的使用方式是在一个写操作之前插入该内存屏障,其作用是是禁止该写操作与前面的任何读写操作之间进行重排序。
      相当于基本内存屏障的StoreStore+LoadStore组合
    • 加载屏障
      加载屏障的作用是刷新同步处理器缓存
      可以由LoadLoad屏障表示,清空无效化队列实现,保证读到值都有效。
    • 存储屏障
      存储屏障的作用是冲刷处理器缓存(应该可以用StoreStore表示,StoreLoad屏障可以一起实现加载屏障和存储屏障)。

    volatile作用

    保障可见性、保障有序性和保障long/double型变量读写操作的原子性。

    volatile原理

    原子性保障

    某些32位的JVM对long/double型变量进行的写操作可能不具有原子性,通过Volatile可以进行保障。但是只保障long/double型变量读写操作本身的原子性,不表示对volatile变量的赋值操作一定具有原子性。

    可见性和有序性

    • 写操作
      Volatile写操作,JVM会在操作前插入一个释放屏障,在操作后插入一个存储屏障。释放屏障禁止了Volatile写操作与该操作之前的任何读写操作进行重排序,保证Volatile写操作之前的任何读写操作都会先于volatile写操作被提交,即其他线程看到写线程对volatile变量的更新时,写线程在更新volatile变量之前所执行的内存操作结果对于读线程均是可见的。保障了读线程对写线程在更新volatile变量前对共享变量所执行的更新操作的感知顺序与相应的源代码一致,即保障了有序性。volatile变量写操作后面的存储屏障具有冲刷处理器缓存的作用,可以使屏障前所有操作的结果对其他服务器是可同步的。
      有序性由释放屏障保障。
      结构如下:
    释放屏障
    Volatile写操作
    存储屏障
    

    JVM(JIT)会在volatile变量写操作之后插入一个StoreLoad,禁止该屏障后任何读操作与屏障前的写操作之间的重排序,此外它充当了存储屏障(冲刷写缓冲器),将写操作的结果送到高速缓存。
    前面应该会插入StoreStore+LoadStore组合,充当释放屏障。

    • 读操作
      Volatile变量读操作,JVM在操作前面插入一个加载屏障,在操作后面插入一个获取屏障。加载屏障通过冲刷处理器缓存,使其执行线程所在处理器将其他服务器对共享变量所做的更新同步到该处理器的高速缓存中。读线程执行的加载屏障和写线程的存储屏障配合在一起使得写线程对volatile变量的写操作以及在此之前所执行的其他内存操作对读线程可见,即保障了可见性(volatile变量自身和写操作之前的所有操作的结果对读线程可见)。另外获取屏障禁止volatile读操作之后的任何读写操作和volatile读操作进行重排序,保障了volatile读操作之后的任何操作开始执行之前,写操作对相关共享变量的更新已经对当前线程可见。
    加载屏障
    读操作
    获取屏障
    

    JVM(JIT)会在volatile变量读操作前插入一个加载屏障(LoadLoad),清空无效化队列,保证读操作可以读到新值。
    后面应该会插入LoadLoad+LoadStore组合,用于充当获取屏障。

    • 跨服务器架构
      X86只支持StoreLoad重排序,故X86下LoadLoad、LoadStore、StoreStore都被映射为空指令,所以在X86情况下,只会在volatile变量写操作后插入StoreLoad屏障,其他处理器架构下,需要在volatile变量读写操作前后加入合适的屏障。

    JIT提示优化

    此外,volatile关键字会给JIT编译器一个提示,从而使JIT不会对相应代码做出一些优化导致可见性问题。

    局限

    可以获取volatile变量的相对新值,无法获取最新值,因为volatile没有加锁,不具备排他性,可能在读取volatile变量的那一刻,volatile变量被其他线程修改,volatile读到的不是最新的值。
    Volatile如果作用于数组,只对数组引用本身的操作(如更新数组引用)起作用,而无法对数组元素的操作起作用。如果要对数组元素的读写操作也能触发volatile的作用,可以使用AtomicIntegerArray\AtomicLongArray\ AtomicRefernceArray

    开销

    volatile变量使用开销高于普通变量,应为使用了内存屏障,使内存重排序、指令重排序等功能受限;volatile变量使用开销低于锁,因为没有锁争用,没有上下文切换。

    使用场景

    • 使用volatile变量作为状态标记;
    • 使用volatile保证可见性;
    • 一定程度代替锁,例如将多个变量封装到一个变量用volatile更新
  • 相关阅读:
    .net编程扫盲(*)
    接口编程扫盲(多态)
    (转)栈与堆栈的区别
    (转).NET基础拾遗(5)多线程开发基础
    (转)你应该知道的计算机网络知识
    网络代理的基础知识
    某代理网站免费IP地址抓取测试
    常用Maven插件介绍
    Maven打jar发布包的常用配置
    Apache Commons CLI 开发命令行工具示例
  • 原文地址:https://www.cnblogs.com/lllliuxiaoxia/p/15718601.html
Copyright © 2011-2022 走看看