volatile 关键字

2021/7/4 23:51:04

本文主要是介绍volatile 关键字,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

对一个 volatile 变量的单个读/写操作,与对一个普通变量的读/写操作使用同一个锁来同步,它们之间的执行效果相同;也就是说对一个 volatile 变量的读,总是能看到(任意线程) 对这个 volatile 变量最后的写入。

值得注意的是:Java 内存模型对 volatile 语义的扩展保证了 volatile 变量在一些情况下不会重排序,volatile 的 64 位变量 double 和 long 的读取和赋值操作都是原子的。如果是多个 volatile 操作或类似于 volatile++ 这种复合操作,这些操作整体上不具有原子性。

简而言之,volatile 变量自身具有一下特性:

  • 可见性:对一个 volatile 变量的读,总是能看到(任意线程) 对这个 volatile 变量最后的写入。
  • 原子性:对任意单个 volatile 变量的读/写具有原子性,但是类似于 volatile++ 这种符合操作不具有原子性。

volatile 写-读的内存语义

当写一个 volatile 变量时,JMM 会把该线程栈中的共享变量值刷新到主内存,使主内存和该线程栈中的共享变量的值是一致的。

当读一个 volatile 变量时,JMM 会把该线程栈中的共享变量的值置为无效,随后线程会从主内存中重新读取共享变量。

下面对 volatile 写和 volatile 读的内存语义做个总结:

  • 线程 A 写一个 volatile 变量,实质上是线程 A 向接下来将要读这个 volatile 变量的某个线程发出了(这个变量已经被修改的)信息。
  • 线程 B 读一个 volatile 变量,实质上是线程 B 接收了之前某个线程发出的(在写这个 volatile 变量之前对共享变量所做修改的)信息。
  • 线程 A 写一个 volatile 变量,随后线程 B 读这个 volatile 变量,这个过程实质上是线程 A 通过主内存想线程 B 发送消息。

简而言之:线程 A 修改了 volatile 变量,通过主内存发送消息到下一个要读 volatile 变量的线程;然后读的这个线程会将线程栈中的这个 volatile 变量置为无效,然后重新从主内存中读取。

volatile 内存语义的实现

volatile 实现 JVM 必须遵循以下重排序规则:

从上表我们可以看出:

  • 当第二个操作是 volatile 写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。
  • 当第一个操作是 volatile 读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile 读之后的操作不会被编译器重排序到 volatile 都之前。
  • 当第一个操作是 volatile 写,第二个操作是 volatile 读时,不能重排序。

留白的单元格代表允许在不违反Java基本语义的情况下重排序。例如,编译器不会对对同一内存地址的读和写操作重排序,但是允许对不同地址的读和写操作重排序。

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止听顶类型的处理器重排序。下面是基于保守策略的 JMM 内存屏障插入策略:

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

下面是保守策略下,volatile 写插入内存屏障后生成的指令序列示意图:

上图中的 StoreStore 屏障可以保证在 volatile 写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为 StoreStore 屏障将保障上面所有的普通写在 volatile 写之前刷新到主内存。

StoreLoad 屏障避免 volatile 写与后面可能有的 volatile 读/写操作重排序。因为编译器常常无法准确判断在一个 volatile 写的后面,是否需要插入一个 StoreLoad 屏障(比如,一个 volatile 写之后方法立即 return)。为了保证能正确实现 volatile 的内存语义,JMM 在这里采取了保守策略:在每个 volatile 写的后面或在每个 volatile 读的前面插入一个 StoreLoad 屏障。从整体执行效率的角度考虑,JMM 选择了在每个 volatile 写的后面插入一个 StoreLoad 屏障。

下面是在保守策略下,volatile 读插入内存屏障后发生的指令序列示意图:

上图的 LoadLoad 屏障用来禁止处理器把上面的 volatile 读与下面的普通读重排序。LoadStore 屏障用来禁止处理器吧上面的 volatile 读与下面的普通写重排序。

保证可见性

保证有序性

保证



这篇关于volatile 关键字的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程