深入理解JVM之volatile变量(五)

关键字volatile可以说是java虚拟机提供的最轻量级的同步机制。当一个变量定义为volatile之后,它将具备两种特性,内存可见性和禁止重排序,至于后者在前一篇介绍先行发生原则中,其中有一条便是由volatile变量构成的原则,具体内容如下。

内存可见性

这里的可见性是指当一条线程修改了这个变量的值,新值对其他线程来说是可以立即得知的。Volatile变量具有synchronized的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • 对变量的写操作不依赖于当前值(这句话需要仔细理解,比如自增操作)。
  • 该变量没有包含在具有其他变量的不变式中。

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x 的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)
要始终牢记使用 volatile 的限制 —— 只有在状态真正独立于程序内其他内容时才能使用 volatile —— 这条规则能够避免将这些模式扩展到不安全的用例。其中一种典型的模式应用就是将 volatile 变量作为状态标志使用,很多应用程序包含了一种控制结构,形式为 “在还没有准备好停止程序时再执行一些工作”,如下列代码所示:

1
2
3
4
5
6
7
8
9
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}

很可能会从循环外部调用 shutdown() 方法 —— 即在另一个线程中 —— 因此,需要执行某种同步来确保正确实现 shutdownRequested 变量的可见性。由于 volatile简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile。

禁止指令重排

使用volatile的第二个语义是禁止指令重排,普通的变量仅仅会保证在该方法执行过程中所有依赖赋值结果的地方都能取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这就是JMM中所谓的“线程内表现为串行的语义”,举例代码如下:

1
2
3
4
5
6
7
8
9
10
11
//全局变量
volatile boolean open=true;
//线程A
resource.close();
open = false;
//线程B
while(open) {
doSomethingWithResource(resource);
}

如果定义open变量没有使用volatile修饰,就可能由于指令重排的优化,导致线程A最后一句的代码被提前执行。

解决了volatile的语义问题,再来看看在众多保障并发安全的工具中选用volatile的意图——它能让我们的代码更快吗?在某些情况下,volatile的同步机制的性能确实要优于锁,但是优于虚拟机对锁实行的许多优化和消除,使我们很难认为volatile就会比synchronized快多少。但是我们可以确定volatile的读操作性能与普通变量没啥区别,写操作要慢些,因为它要在本地代码中插入许多内存屏障来保证处理器不发生乱序执行。我们在volatile和锁之间选择的唯一依据仅仅是volatile的语义能否适用场景的需求。