共享变量的内存可见性

谈到内存可见性,我们首先来看看在多线程下处理共享变量时 Java 的内存模型,如图 2-4 所示。

Volatile关键字 - 图1

Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量。Java 内存模型是一个抽象的概念,那么在实际实现中线程的工作内存是什么呢?请看图 2-5。

Volatile关键字 - 图2

图中所示是一个双核 CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辑运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有 CPU 都共享的二级缓存。那么 Java 内存模型里面的工作内存,就对应这里的 L1 或者 L2 缓存或者 CPU 的寄存器。

当一个线程操作共享变量时,它首先从主内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完后将变量值更新到主内存。

那么假如线程 A 和线程 B 同时处理一个共享变量,会出现什么情况?我们使用图 2-5 所示 CPU 架构,假设线程 A 和线程 B 使用不同 CPU 执行,并且当前两级 Cache 都为空,那么这时候由于 Cache 的存在,将会导致内存不可见问题,具体看下面的分析。

● 线程 A 首先获取共享变量 X 的值,由于两级 Cache 都没有命中,所以加载主内存中 X 的值,假如为 0。然后把 X=0 的值缓存到两级缓存,线程 A 修改 X 的值为 1,然后将其写入两级 Cache,并且刷新到主内存。线程 A 操作完毕后,线程 A 所在的 CPU 的两级 Cache 内和主内存里面的 X 的值都是 1。

● 线程 B 获取 X 的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回 X= 1;到这里一切都是正常的,因为这时候主内存中也是 X=1。然后线程 B 修改 X 的值为 2,并将其存放到线程 2 所在的一级 Cache 和共享二级 Cache 中,最后更新主内存中 X 的值为 2;到这里一切都是好的。

● 线程 A 这次又需要修改 X 的值,获取时一级缓存命中,并且 X=1,到这里问题就出现了,明明线程 B 已经把 X 的值修改为了 2,为何线程 A 获取的还是 1 呢?这就是共享变量的内存不可见问题,也就是线程 B 写入的值对线程 A 不可见。

那么如何解决共享变量内存不可见问题?使用 Java 中的 volatile 关键字就可以解决这个问题。

volatile 关键字

使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太笨重,因为它会带来线程上下文的切换开销。对于解决内存可见性问题,Java 还提供了一种弱形式的同步,也就是使用 volatile 关键字。该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile 的内存语义和 synchronized 有相似之处,具体来说就是,当线程写入了 volatile 变量值时就等价于线程退出 synchronized 同步块(把写入工作内存的变量值同步到主内存),读取 volatile 变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。

下面看一个使用 volatile 关键字解决内存可见性问题的例子。如下代码中的共享变量 value 是线程不安全的,因为这里没有使用适当的同步措施。

  1. public class ThreadNotSafeInteger {
  2. private int value;
  3. public int get() {
  4. return value;
  5. }
  6. public void set(int value) {
  7. this.value = value;
  8. }
  9. }

首先来看使用 synchronized 关键字进行同步的方式。

  1. public class ThreadSafeInteger {
  2. private int value;
  3. public synchronized int get() {
  4. return value;
  5. }
  6. public synchronized void set(int value) {
  7. this.value = value;
  8. }
  9. }

然后是使用 volatile 进行同步。

  1. public class ThreadSafeInteger {
  2. private volatile int value;
  3. public int get() {
  4. return value;
  5. }
  6. public void set(int value) {
  7. this.value = value;
  8. }
  9. }

在这里使用 synchronized 和使用 volatile 是等价的,都解决了共享变量 value 的内存可见性问题,但是前者是独占锁,同时只能有一个线程调用 get()方法,其他调用线程会被阻塞,同时会存在线程上下文切换和线程重新调度的开销,这也是使用锁方式不好的地方。而后者是非阻塞算法,不会造成线程上下文切换的开销。

但并非在所有情况下使用它们都是等价的,volatile 虽然提供了可见性保证,但并不保证操作的原子性。

那么一般在什么时候才使用 volatile 关键字呢?

● 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取—计算—写入三步操作,这三步操作不是原子性的,而 volatile 不保证原子性。

● 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为 volatile 的。

MESI与volatile

既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?

首先,volatile是java语言层面给出的保证,MSEI协议是多核cpu保证cache一致性(后面会细说这个一致性)的一种方法,中间隔的还很远,我们可以先来做几个假设:

  1. 回到远古时候,那个时候cpu只有单核,或者是多核但是保证sequence consistency[1],当然也无所谓有没有MESI协议了。那这个时候,我们需要java语言层面的volatile的支持吗?当然是需要的,因为在语言层面编译器和虚拟机为了做性能优化,可能会存在指令重排的可能,而volatile给我们提供了一种能力,我们可以告诉编译器,什么可以重排,什么不可以。
  2. 那好,假设更进一步,假设java语言层面不会对指令做任何的优化重排,那在多核cpu的场景下,我们还需要volatile关键字吗?答案仍然是需要的。因为 MESI只是保证了多核cpu的独占cache之间的一致性,但是cpu的并不是直接把数据写入L1 cache的,中间还可能有store buffer。有些arm和power架构的cpu还可能有load buffer或者invalid queue等等。因此,有MESI协议远远不够。
  3. 再接着,让我们再做一个更大胆的假设。假设cpu中这类store buffer/invalid queue等等都不存在了,cpu是数据是直接写入cache的,读取也是直接从cache读的,那还需要volatile关键字吗?你猜的没错,还需要的。原因就在这个“一致性”上。consistency和coherence都可以被翻译为一致性,但是MSEI协议这里保证的仅仅coherence而不是consistency。那consistency和cohence有什么区别呢?下面取自wiki[2]的一段话:

Coherence deals with maintaining a global order in which writes to a single location or single variable are seen by all processors. Consistency deals with the ordering of operations to multiple locations with respect to all processors.

因此,MESI协议最多只是保证了对于一个变量,在多个核上的读写顺序,对于多个变量而言是没有任何保证的。很遗憾,还是需要volatile~~

  1. 好的,到了现在这步,我们再来做最后一个假设,假设cpu写cache都是按照指令顺序fifo写的,那现在可以抛弃volatile了吧?你觉得呢?我都写到标题4了,那肯定不行啊!因为对于arm和power这个weak consistency[3]的架构的cpu来说,它们只会保证指令之间有比如控制依赖,数据依赖,地址依赖等等依赖关系的指令间提交的先后顺序,而对于完全没有依赖关系的指令,比如x=1;y=2,它们是不会保证执行提交的顺序的,除非你使用了volatile,java把volatile编译成arm和power能够识别的barrier指令,这个时候才是按顺序的。

最后总结上文,答案就是:还需要~~

Reference

[1] https://en.wikipedia.org/wiki/Sequential_consistency

[2] https://en.wikipedia.org/wiki/Consistency_model

[3] Maranget, Luc, Susmit Sarkar, and Peter Sewell. “A tutorial introduction to the ARM and POWER relaxed memory models.” Draft available from http://www. cl. cam. ac. uk/~ pes20/ppc-supplemental/test7. pdf (2012).