理解 Java volatile 关键字

概述

volatile 是 Java 中最轻量的同步机制,可以保证变量在各个线程之间的可见性。具体来讲,volatile 会使得线程每次都将从主存而不是 CPU 缓存读取变量,每次将更新写回主存而不仅仅是写入缓存。

我们知道每个 CPU 都有自己的缓存,这样在多线程环境下,每个线程都有一份自己的变量拷贝。non-volatile 变量不能保证该变量在各个线程之间的一致性。一个例子如下:

两个或者多个线程同时访问一个共享对象,该对象包含一个 counter 变量:

public class SharedObject{
    public int counter = 0;
}

只有线程 1 会自增 counter 变量,但是线程 1 和线程 2 都会时不时读取 counter 变量。

如果 counter 变量没有被声明为 volatile,那么将不能保证变量被修改后,会被立即写入到主内存。这就意味着,当线程 1 更改了 counter 的值,线程 2 并不能立即看到 counter 的更改。如下图所示: visibility

这一问题可被归纳为”可见性”问题:线程 2 没有看到 counter 最新的值是因为线程 1 只是将值写回缓存而没有写回主存。

这一问题可以通过将变量声明为 volatile 来解决。当 counter 被声明为 volatile 时,线程 1 对 counter 的更新会被立即写回到主内存。同时,线程 1 和线程 2 读取 counter 时,都将会直接从主内存中读取。将一个变量声明为 volatile,可以保证变量对所有线程都具有可见性。

Happens-Before 保证

从 Java 5 开始,volatile 还可以保证:

  • if Thread A writes to a volatile variable and Thread B subsequently reads the same volatile variable, then all variables visible to Thread A before writing the volatile variable, will also be visible to Thread B after it has read the volatile variable.
  • The reading and writing instructions of volatile variables cannot be reordered by the JVM ( the JVM may reorder instructions for performance reasons as long as the JVM detects no change in program behaviour from the reordering). Instructions before and after can be reordered, but the volatile read or write cannot be mixed with these instructions. Whatever instructions follow a read or write of a volatile variable are guaranteed to happen after the read or write.

当一个线程更新 volatile 变量时,那么不仅仅是该 volatile 变量会被写回内存,同时该线程在更新 volatile 变量前修改的所有其它变量都会被写入到主内存。当其他线程读取该 volatile 变量时,也会读取到其他被写回到主内存的变量。如:

Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter = sharedObject.counter + 1;
    
Thread B:
    int counter = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile

因为线程 A 在写 volatile 变量 sharedObject.counter 之前写入了 non-volatile 变量 sharedObject.nonVolatile,那么当线程 A 写完 sharedObject.counter 变量, sharedObject.nonVolatile 和 sharedObject.counter 都会被写回到主内存。

当线程 B 开始读取 volatile 变量 sharedObject.counter,还会从主内存读取 sharedObject.nonVolatile 到自己的 CPU 缓存中。

Happens before 保证 volatile 变量的读和写都不会被重排,而 volatile 变量之前和之后的指令可以重排序,但是不能将 volatile 读/写指令与其他指令做重排序。一个例子如下:

sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;

sharedObject.volatile = true; // a volatile variable

int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;

JVM 可以重排序前三个赋值语句,只要它们 happens before 该 volatile 写指令(即它们必须在 volatile 写指令前执行,而不能将它们与 volatile 写重排序)。类似的,JVM 也可以重排序后面三个赋值语句,只要 volatile 写指令 happens before 它们。(即它们必须在 volatile 写指令之后执行,也不能将这三个指令与 volatile 写重排序)。

正确使用 volatile

多线程环境下,使用 volatile 必须满足以下条件才能保证线程安全:

  • Writes to the variable do not depend on its current value.
  • The variable does not participate in invariants with other variables.

[4] 中介绍了多线程环境下使用 volatile 的常见几种使用模式。

局限

volatile 只可以保证可见性,不能保证原子性。如果需要保证原子性,必须使用 synchronized 或者锁。

参考

  1. 深入理解 Java 虚拟机
  2. Java 并发编程的技术
  3. Java Volatile Keyword
  4. Managing volatility

Previous post: 理解 Java 关键字 synchronized

Next post: 理解 Java Future