volatile底层实现原理

《Java并发编程的艺术》这本书的每一节基本上都是面试中的高频考点或者是重难点,实体书在书桌上摆了许久竟然都忽视掉了,现在必须要把这本书整理成电子版的。

Java并发编程的艺术

volatile的应用

volatile是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。volatile 变量不会引起线程上下文的切换和调度。

可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

volatile的定义与实现原理

在继续深入原理之前,需要先了解几个 CPU 术语。

CPU的术语定义

对于 volatile 是如何保证可见性的,需要查看 JIT 编译器生成的汇编指令来查看对 volatile 进行写操作时,CPU会做什么事情。

1
instance = new Singleton(); // instance是volatile变量

转换成汇编代码

1
2
0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);

被 volatile 变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,其中 lock是关键。Lock 前缀的指令在多核处理器下会引发了两件事情:

  • 将当前处理器缓存行的数据写回到系统内存。
  • 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

volatile的两条实现原则:

  • Lock前缀指令会引起处理器缓存回写到内存。
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。

指令序列的重排序类型

  • 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序:

从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:

从源码到最终执行的指令序列的示意图

上述的1属于编译器重排序,2和3属于处理器重排序。

  • 对于编译器重排序:JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
  • 对于处理器重排序:JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为 Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
屏障类型 指令示例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 确保 Load1 数据的装载先于 Load2 及所有后续装载指令的装载
StoreStore Barriers Store1;StoreStore;Store2 确保 Store1数据对其他处理器可见(刷新到内存)先于 Store2 及所有后续存储指令的存储
LoadStore Barriers Load1;LoadStore;Store2 确保Load1数据装载先于 Store2 及所有后续的存储指令刷新到内存
StoreLoad Barriers Store1;StoreLoad;Load2 确保 Store1 数据对其他处理器变得可见(指刷新到内存)先于 Load2 及所有后续装载指令的装载。StoreLoad Barriers 会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。

volatile的特性

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

从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果;volatile和锁的释放有相同的内存语义;volatile与锁的获取有相同的内存语义。

volatile的内存语义

  • volatile写:JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
  • volatile读:JMM会把该线程对应的本地内存置为无效,然后再从主内存中读取共享变量。实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,实质上是线程A通过主内存向线程B发送消息。

volatile内存语义的实现

重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM 会分别限制这两种类型的重排序类型。

volatile重排序规则表

从中可以看出:

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

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

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

由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大;在可伸缩性和执行性能上,volatile更有优势。

评论