转载

[Java 并发]你确定你了解 volatile ?

学 Java 并发,过不去 volatile 和 synchronized ,既然过不去,那就不过了,踏踏实实把它搞懂,踩在脚下.

这篇文章先搞定 volatile ,后面我再写另外一篇文章关于 synchronized 和锁的.
以下,正文开始:

在 Java 中, volatile 主要有两个功能:

  • 保证变量的内存可见性
  • 禁止 volatile 变量与普通变量重排序

接下来一一来看这两个功能,以及是怎么实现的

什么是内存可见性

如果要谈 volatile 保证了变量的内存可见性,那就需要了解什么是内存可见性

所谓内存可见性是说,当一个线程对 volatile 修饰的变量进行 写操作 时, JMM 会立即将该线程对应的本地内存中的共享变量的值刷新到主内存中;当一个线程对 volatile 修饰的变量进行 读操作 时, JMM 会立即将该线程对应的本地内存设置为无效,然后从主内存中读取共享变量的值

在 JSR-133 之前的旧的 Java 内存模型中,是允许 volatile 变量与普通变量重排序的.
也就是说,虽然 volatile 变量能够保证内存可见性,但是可能程序执行的结果依旧不是你想要的.
如果直接使用锁的话,又会让整个程序变得比较重量级,基于以上考虑, JSR-133 专家组决定增强 volatile 的内存语义: 严格限制编译器和处理器对 volatile 变量与普通变量的排序

如何禁止 volatile 变量与普通变量重排序

俗话说,说得容易,做起来就比较难.定义了严格限制 volatile 变量与普通变量的排序,那是拿什么来做保证的呢? JVM 在处理器层面是通过 内存屏障 来实现的.

  • 什么是内存屏障呢?从硬件层面来说,内存屏障分为两种:读屏障( Load Barrier )和写屏障( Store Barrier ).内存屏障有两个作用:
    • 阻止屏障两侧的指令重排序
    • 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效.

      这里的缓存主要是指: CPU 缓存,如 L1 , L2 等

编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序.
在这里编译器选择了一个比较保守的 JMM 内存屏障插入策略,保守的好处就是,可以保证在任何处理器平台,任何程序中都能得到正确的 volatile 内存语义.这个保守策略就是( Load 代表读操作, Store 代表写操作):

  • 在每个 volatile 写操作前,插入一个 StoreStore 屏障;
    • 比如: Store1 ; StoreStore ; Store2 语句,在 Store2 及后续写入操作执行前,要保证 Store1 的写入操作对其他处理器可见
  • 在每个 volatile 写操作后,插入一个 StoreLoad 屏障;
    • 比如: Store1 ; StoreLoad ; Load2 语句,在 Load2 及后续所有读取操作之前,要保证 Store1 的写入对所有处理器可见
  • 在每个 volatile 读操作后,插入一个 LoadLoad 屏障;
    • 比如: Load1 ; LoadLoad ; Load2 ,在 Load2 及后续读取操作要读取的数据被访问前,要保证 Load1 要读取的数据读取完毕
  • 在每个 volatile 读操作后,再插入一个 LoadStore 屏障
    • 比如: Load1 ; LoadStore ; Store2 在 Store2 及后续写入操作被刷出前,要保证 Load1 读取的数据读取完毕

是不是有点儿懵?别急,我这里画了两张图,可以看着理解一下

[Java 并发]你确定你了解 volatile ? [Java 并发]你确定你了解 volatile ?

写到这里了,就顺便介绍一下 volatile 和普通变量的重排序规则:

  • 如果第一个操作是 volatile 读,那么不管第二个操作是什么,都不能重排序;
  • 如果第二个操作是 volatile 写,那么不管第一个操作是什么,都不能重排序;
  • 如果第一个操作是 volatile 写,第二个操作是 volatile 读,也不能重排序;

可以发现,针对 volatile 写操作来说,是比较严格的,但是如果第一个是普通变量的读,第二个是 volatile 的读,我可不可以重排序呢?可以

volatile 怎么用

看到这里,应该就能知道, volatile 保证了内存可见性以及禁止重排序.
在保证内存可见性这一点上,可以说 volatile 和锁有着相同的意义,所以 volatile 可以作为一个”轻量级”锁来使用.
volatile 的本质其实就是告诉 JVM ,我修饰的这个变量在寄存器中的值是不确定的,如果需要的话,不能直接从本地内存中读取,需要从主存中去拿,所以 volatile 它改变的只是变量的可见性,但是不保证原子性.
基于此,就需要搞清楚,在什么情况下使用 volatile 比较好.

对于 volatile 关键字来说,当且仅当满足以下所有条件时,才可以使用:

  • 对变量的写操作不依赖变量的当前值,或者确保只有单个线程更新变量的值
  • 变量没有包含在具有其他变量的不变式中

我觉得上面的条件,就是为了保证操作是原子性操作,因为 volatile 不保证原子性,那为了安全,就要保证你本身的操作就是原子性操作,相当于直接从源头上就把不是原子性操作给排除掉.
这样的话,就比较容易搞清楚 volatile 这个变量使用在什么场景下了:

  • 用来标识状态,比如 boolean flag 这种
  • 一次性安全发布( one-time safe publication ):实现安全发布对象的一种技术就是将对象引用定义为 volatile 类型
  • 独立观察( independent observation):安全使用 volatile 的另一种简单模式是:定期”发布” 观察结果供程序内部使用.例如,假设有一种环境传感器能够感觉环境温度,一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量.然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值

参考:

深入理解 JVM

Java 理论与实践:正确使用 Volatile 变量

并发关键字 volatile(重排序和内存屏障)

JMM——volatile与内存屏障

以上,感谢您的阅读哇

原文  https://www.dynamic-zheng.com/posts/6bdf9b1b.html
正文到此结束
Loading...