volatile关键字用于将一个Java变量标记为“储存在主内存中”。更精确的意思是,对volatile变量的读操作都从计算机的主内存获取,而非从CPU缓存;对volatile变量对写操作会被立即同步到主内存,不只是更新CPU缓存。
事实上,自从Java5之后,volatile关键字拥有更多的含义。

Variable Visibility Problems

volatile关键字保证对变量的修改跨线程可见。听起来也许有点抽象,我将详细阐述。
在一个多线程应用中,有多个线程操作非volatile变量,出于性能的考虑,每个线程在工作过程中会从主内存里拷贝这些变量到CPU缓存中。如果你的计算机拥有多个CPU,每个线程可能运行在不同的CPU上。那意味着,每个线程都可能拷贝这些变量到不同的CPU缓存中,见下图:volatile关键字 - 图1在变量没
有volatile修饰的情形下,无法保证Java虚拟机何时将数据从主内存读到CPU缓存,或从CPU缓存写到主内存。这会引起一系列问题,我将在后续章节中解释。想象一个情形:两个或更多线程访问一个共享对象,该对象包含一个计数器变量
public class SharedObject {
public int counter = 0;
}
假设,只有线程1增加这个计数器变量,而线程1和线程2会时而读取该变量。
如果该计数器变量没有被声明为volatile,那么无法保证它的值何时从CPU缓存写回主内存。这意味着,CPU缓存中的值 可能与主内存中的计数值不相同,见下图:
volatile关键字 - 图2
线程1使用的CPU缓存与主内存持有不同的计数器变量值。
之所以出现这种线程无法“看见”最新变量值的问题,是因为变量的最新值还未被其它线程写回主内存,这被称作可见性问题。一个线程的更新操作对其它线程不可见。

Java volatile可见性保障

volatile关键字旨在解决变量可见性问题。通过将计数器变量声明为volatile,可以使所有对计数器变量的写操作都立即同步到主内存,所有对计数器变量的读操作都直接从主内存获取。
以下是如何将计数器变量声明为volatile
public class SharedObject {
public volatile int counter = 0;

}
将变量声明为volatile,从而保证了其它线程对该变量写操作的可见性。
在以上情景中,一个线程(T1)更改计数器,另外一个线程(T2)读计数器(从不更改它),将计数器变量声明为volatile就足以保证T2对计数器变量的可见性。
然而,如果T1和T2都增加该计数器变量,那么仅仅声明计数器变量为volatile就不够了。后面再讨论。

完全的volatile可见性保障

事实上,volatile的可见性保证已经超出了volatile变量本身:

  • 如果线程A写一个volatile变量,而线程B随后读该变量,那么所有在写volatile变量之前对线程A可见的变量,在线程B读了volatile变量之后,也对线程B可见
  • 如果线程A读一个volatile变量,那么所有在线程A读volatile时对线程A可见的变量,也会被重新从主内存中读取。

让我们通过代码示例来解释:
public class MyClass {
private int years;
private int months
private volatile int days;
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
update()方法写了三个变量,只有days变量被修饰为volatile。
完全的volatile可见性保证意味着,当有一个值被写到days变量时,那么所有对线程可见的变量都被写到主内存。这意味着,当days被赋值,years和months的值也会被写到主内存。
当读years、months和days的值时,你可以这样做:
public class MyClass {
private int years;
private int months
private volatile int days;
public int totalDays() {
int total = this.days;
total += months 30;
total += years
365;
return total;
}
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
请注意,totalDays()方法首先将days变量的值读进total变量。当读days的值时,months和years的值也被从主内存读到CPU缓存。
因此,通过以上的读顺序,可以保证你能够看见days、months和years的最新值。

指令重排序带来的挑战

基于性能考虑,Java虚拟机和CPU在指令的语义含义保持不变的前提下,会对指令进行重排序。举例如下:
int a = 1;
int b = 2;
a++;
b++;
这些指令可能被重新排序为以下顺序(语义没有变化)
int a = 1;
a++;
int b = 2;
b++;
然而,当变量被声明为volatile时,指令重排序会带来一个挑战。让我们继续看这个示例:
public class MyClass {
private int years;
private int months
private volatile int days;
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
一旦update()方法向days写入一个值,最近写入years和months的值也被写到主内存。但是,如果Java虚拟机重新排序了指令,像这样:
public void update(int years, int months, int days){
this.days = days;
this.months = months;
this.years = years;
}
当days变量被修改时,变量months和years的值仍然会被写回主内存,但是这发生在months和years变量被赋予新值之前。这些新值因此无法对其它线程保证预期对可见性。重排序后的指令语义发生了变化。
针对这个问题,Java有一个解决方案,我们将在下一个章节看到。

Java volatile Happens-Before 原则

为了解决指令重排序带来的挑战,volatile关键字提供了一个”happens-before”保证,作为对可见性保证的补充。该happens-before原则保证:

  • 对其它变量的读写操作,不能被重排序到对一个volatile变量的写操作之后,如果这些读写操作原本就在对volatile变量的写操作之前。即这些在写volatile变量之前的读写操作得以保证“happen before”对volatile变量的写操作。注意,在写volatile变量之后对其它变量的读写操作依然有可能被重排序到写volatile变量操作之前。可以从后往前,不能从前往后。
  • 对其它变量的读写操作,不能被重排序到对一个volatile变量的读操作之前,如果这些读写操作原本就在对volatile变量的读操作之后。注意,在读volatile变量之前对其它变量的读写操作依然有可能被重排序到读volatile变量操作之后。可以从前往后,不能从后往前。

以上happens-before原则确保了volatile关键字的可见性保障。

volatile 并非总是够用

即使volatile关键字保证了所有的读操作直接从主内存读取,所有的写操作直接同步到主内存,仍然有些情形是声明volatile变量不能解决的。
在前面提到的案例中,只有线程1写共享的计数器变量,将计数器变量声明为volatile足以保证线程2总是能看到最新被写入的值。
事实上,多个线程写共享volatile变量的情形下,依然可以保证主内存中存储的值是正确的,如果新写入的值不依赖于之前的值。也就是说,如果一个线程在向一个共享volatile变量写入值之前,不需要首先读它的值以计算下一个值。
一旦一个线程需要先读一个volatile变量的值,并基于这个值来生成一个新的值赋给volatile变量,volatile就无法保证正确的可见性。在读旧值和写新值之间的的短暂间隙,制造了一个竞争条件:多个线程可能读取到同样的volatile变量值,生成一个新值,并将各自生成的值写回主内存。
多个线程累增同一个计数器的情形正是这种volatile不够用的情形。下面将更详细地解释这个例子。
想象一下,如果线程1从共享的计数器变量读取值0到CPU缓存,将其递增到1,没有将改变后的值写回到主内存。线程2可能从主内存中读取计数器变量的旧值0到它的CPU缓存中。线程2也将其递增到1,并且也没有将其写回主内存。这种情形可以通过下图阐明:
image.png
线程1和线程2现在实际上不同步。该共享计数器变量本应是2,但是各个线程在各自的CPU缓存中持有的值是1,且主内存中,该变量值依然是0。这简直一团糟!即使各线程最终会将各自持有的副本同步到主内存,这个值仍然是错误的。

什么时候volatile是够用的呢

正如我前面提到的,如果两个线程同时读写一个共享变量,那么使用volatile关键字是不够的。你需要使synchronized,保证读和写变量是原子性的。读或写一个volatile变量不会阻塞其它线程的读写操作。为了达到这点,你需要在特定区域使用synchronized关键字。
除了使用synchronized关键字阻塞其它线程,你还可以使用java.util.concurrent包中的许多原子数据类型。比如AtomicLong或AtomicReference。
在只有一个线程读写volatile变量,其它线程只读变量的情形,读线程能够保证看见写入volatile变量的最新值。如果不用volatile修饰变量,将无法保证这一点。
volatile关键字可以使用在32bit和64bit变量上。

volatile的性能考虑

读和写volatile变量会时变量从主内存读取或写回主内存。从主内存读和写主内存比访问CPU缓存慢得多。访问volatile变量也会阻止指令重排序,而指令重排序是一种正常的性能提升技术。因此,你应该在确实需要保证变量可见性的情况下才使用volatile关键字
本文译自:[1]http://tutorials.jenkov.com/java-concurrency/volatile.html

为什么double check 单例模式中需要用volatile

  1. public class ThreadSafeSingleton {
  2. private static volatile ThreadSafeSingleton instance;
  3. private ThreadSafeSingleton(){}
  4. public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
  5. if(instance == null){
  6. synchronized (ThreadSafeSingleton.class) {
  7. if(instance == null){
  8. instance = new ThreadSafeSingleton();
  9. }
  10. }
  11. }
  12. return instance;
  13. }
  14. }

instance = new ThreadSafeSingleton(); 这一步构造对象并赋值的操作并非原子的。可以分为三步:1.开辟内存区域、2.构造新对象、3.将instance指针指向新开辟的内存区域。如果没有将instance变量声明为volatile,jvm可能以132的顺序执行这三条指令,这就意味着在代码第8行的判断处,instance可能不为null,但是对象却没有完成初始化,导致获得的对象是一个半成品