Java - volatile
Java的关键字 volatile 用于将变量标记为“存储于主内存中”。更确切地说,对 volatile 变量的每次读操作都会直接从计算机的主存中读取,而不是从 cpu 缓存中读取;同样,每次对 volatile 变量的写操作都会直接写入到主存中,而不仅仅写入到 cpu 缓存里。
实际上,从 Java 5 开始关键字 volatile 除了能确保 volatile 变量直接从主存中进行读写,还有以下几个作用。
可见性保证
关键字 volatile 能确保数据变化在线程之间的可见性。
在多线程的应用中多个线程对 non-volatile 变量进行操作,线程在对它们进行操作的时候为了提高性能会将变量从主存复制到 cpu 缓存中。如果你的电脑包含的 cpu 不止一个, 那么每个线程可能会运行于不同的 cpu 上。这意味着,不同线程会将变量复制到不同 cpu 的缓存里。如下图:
no-volatile 变量不能保证 Java 虚拟机(JVM)何时从主存中将数据读入cpu 缓存,也不能保证何时将数据从 cpu 缓存写入到主存中。这会带来一些问题,我将在下面解释。
想象一个场景,两个或两个以上线程可访问同一个共享对象,这个对象含有一个如下的计数器变量:
public class SharedObject{
public int counter = 0;
}
再假设有2个线程 Thread1 和 Thread2,只有 Thread1 能增大 counter
,而 Thread1 和 Thread2 都可以在任何时刻读取 counter
的值。
如果 counter
没被声明为 volatile ,将不能保证什么时候 counter
变量的值会从 cpu 缓存回写到主存内。也就是说,变量 counter
在 cpu 缓存中的值可能和主存内的不一致。 如下图:
这种由于线程还未将变量的值回写到主存而导致其他线程不能看到该变量的最新值的问题,称为可见性问题。一个线程的更新操作对其他线程不可见。
通过将变量 counter
声明为 volatile ,对其进行的所有写操作都会马上回写至主存中。同时,所有 counter
的读操作也将直接在主存中进行。声明方式如下:
public class SharedObject{
public volatile int counter = 0;
}
这样,将变量声明为 volatile 保证了写操作对其他线程的可见性。
Happens-before 保证
自 Java 5 之后,关键字 volatile 不仅仅保证变量写入主存和从主存中的读取。实际上,volatile 保证了以下几点:
- 如果线程A写 volatile 变量(下文用
volatile
简称 volatile 变量), 然后线程B 读取这个volatile
,那么在写volatile
之前对线程A可见的变量也将在线程B 读取这个volatile
之后可见。 - 对 volatile 变量的读取和写入指令不能被 JVM 重排序(只要 JVM 识别出程序的行为在重排序后不会改变,它就会对指令进行重排序以提高性能)。操作
volatile
之前和之后的指令可以重排序,但是不能将其和这些指令混在一起重排序。任何发生在volatile
的读写操作之后的指令一定发生在读写操作之后。(具体的可以看本文底部的 “正确使用volatile” 里的说明)
我们来做对以上叙述做进一步的说明:
当线程写入 volatile
时,不单单是将这个 volatile
写入主存中。这个线程在写此
volatile 变量之前改变的所有的变量也将刷新到主存中。当另一个线程读取这个 volatile
变量时,它也能从主存中读取到随 volatile
一起被刷入主存的其他所有变量。
看看这个例子:
Thread A:
sharedObject.nonVolatile = 123;
sharedObject.counter = sharedObject.counter + 1; // volatile
Thread B:
int counter = sharedObject.counter;
int nonVolatile = sharedObject.nonVolatile;
由于线程A 在写 volatile 的变量 sharedObject.counter
之前写 non-volatile 变量 sharedObject.nonVolatile
,sharedObject.counter
和 sharedObject.nonVolatile
会在 写 sharedObject.counter
的时候一起写入到主存中。
由于线程B 开始时先读取 volatile 变量 sharedObject.counter
, 那么 sharedObject.counter
和 sharedObject.nonVolatile
会直接从主存读取到供线程B 使用的 cpu 缓存中 。这个时候,线程B 读到的 sharedObject.nonVolatile
就是线程A 写入的新值。
开发人员可以利用这扩展的可见性保证来优化线程间变量的可见性。只将一个或者几个变量声明为 volatile ,而不是把每个变量都声明成 volatile, 比如常用的标记位变量 flag 就可以放心的在处理完相应才操作后置为 true 了 。利用这个原则来简单地重写 Exchanger
类:
public class Exchanger{
private Object object = null;
private volatile hasNewObject = false;
public void put(Object newObject){
while(hasNewObject){
// 等待 , 不要去覆盖object字段
}
object = newObject; // 在写 volatile 之前进行的普通写
hasNewObject = true; // 写 volatile
}
public Object take(){
while(! hasNewObject){ // 读 volatile
// 等待, 不获取旧的 object或null
}
Object obj = object; // 再写 volatile 之前进行的普通读
hasNewObject = false; // 写 volatile
return obj;
}
}
执行场景为线程A 不断调用 put()
方法塞入新对象,线程B 不断调用 take()
方法获取新对象。如果仅有线程A 调用 put()
并且仅有线程B 调用 take()
, 那么这个 Exchanger
只要使用 volatile
变量就能正常运行了(不需要使用 synchronized 同步代码块)。
然而,如果 JVM 对指令进行重排序后不影响其执行语义,它就会对 Java 指令进行重排序以提高性能。如果 JVM 调整 put()
和
take()
内读写指令的执行顺序,会发生什么? 如果 put()
实际上是按如下顺序执行的会怎样?
while(hasNewObject){
// 等待 , 不要去覆盖object字段
}
hasNewObject = true; // 写 volatile
object = newObject;
注意,现在上面示例中写 volatile
在新的 object
赋值前就执行了。对 JVM 来说这也许看起来完全合法,因为这两个写操作的值彼此之间没有依赖。
不过, 以上的重排序会损害 volatile
变量 object
的可见性。首先,线程B 可能在线程给变量 object
赋新值之前就看到 hasNewObject
已经是 true 了。其次,现在已经不能保证 object
的新值被回写到主存了(也许是下次线程A 在某处写volatile
的时候)。
为了阻止如上情形的发生,关键字 volatile 还提供了 happes before 保证。happens-before 保证对 volatile 变量的读写指令不会被重排序。可以重排序在其之前和之后发生的指令,但是对 volatile 变量的读写指令不能同先于或后于它发生的任何指令一起重排序。
看看如下例子:
sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;
sharedObject.volatile = true; // volatile 变量
int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;
JVM 会重排序前面的3个指令,只要保证它们都在 写 volatile 之前发生就可以(它们必须在写 volatile 指令前执行)。
类似地,JVM 也会重排序最后的 3 个指令,只要保证它们都在写 volatile 之后发生就可以。最后的 3 个指令都不能重排序至 写volatile指令之前。
这是 Java volatile happens-before 保证的基本含义。
volatile对于重排序的禁止操作主要是java编译器在生成指令序列时会在适当的位置插入内存屏障来阻止重排序,具体说明可以参考《java并发编程艺术》3.1 和 3.4.3小节。
volatile 并不能满足所有场景
即使关键字 volatile 能保证对它的所有读操作都是直接从主存中读取,所有写操作也都是直接写入主存中,还是有些仅将变量声明为 volatile 不能满足的场景。
在前面讨论的例子中只有线程A 对共享变量 counter
进行写操作,将 counter
声明为 volatile 就足以保证线程B 能看到新写入的值。
实际上,多线程甚至在同时对共享的 volatile 变量进行写操作时,只要新值的写入不依赖它之前的值,就仍然能保持主存中的值是正确的。换句话说,一个线程将一个值写入共享 volatile 变量中,不需要首先读取原来值来计算下一个新值。
只要线程需要先读取 volatile
的值, 然后基于这个值来生成新值,那么这个 volatile 变量就不再能保证其正确的可见性。读取 volatile
和写入新值之间的时间间隙产生了 竞态条件 ,多个线程可能读取到相同的值, 然后生成新值,接着在将值回写到主存中的时候就会覆盖彼此的值了。
多线程增加同一个计数器 counter 就恰好是 volatile 不够用的一个场景。接下来我们来更详细的解释这个例子。
假设线程1 读取值为0的共享变量 counter
到它的 cpu 缓存中,增加值到1,但还没把改变的值回写到主存中。 线程2 接着也能从主存中读取值还是0这个 counter
,并将其存入它自己的 cpu 缓存里。接着线程2 也将 counter
的值增加到1,同样也还没回写主存。这个场景如下图:
线程1 和线程2 现在就是切实的不同步。共享变量
counter
实际的值应该 2,但是每个线程在各自的 cpu 缓存中的值为 1,主存中的值还是 0。乱成一团了!即使最后线程都把它们持有的值回写到主存中,counter
的值也是错的。
什么时候单单使用 volatile 就够了?
就如之前提到的,如果两个线程都对共享变量进行读写,那么只使用关键字 volatile 就不能满足要求了。这种情况你需要用 synchronized 来保证读写变量的原子性。对 volatile 变量的读写操作并不会阻塞其他线程的读写。如果需要阻塞,你就必须在临界区周围使用关键字 synchronized 。
如果不想用 synchronized 代码块,你可以从包java.util.concurrent中找到很多有用的原子数据类型,如 AtomicLong,AtomicReference 或者其他。
假设只有一个线程同时读写 volatile
变量,其他线程只读取,那么只读线程一定能看到最新写入到 volatile 变量的值。如果不将变量声明为 volatile ,这就的不到保证。
关键字 volatile 确定能在 32位和64位变量上正常运行。
volatile 的性能考虑
对 volatile 变量的读取和写入操作导致变量直接在主存中读写。从主存中读取和写入到主存中比在 cpu 缓存中代价更高 。访问 volatile 变量也阻止了常规的性能优化技术对指令的重排序。所以,你应该只在确实需要加强变量的可见性的时候使用 volatile。
references:
- 英文原文: http://tutorials.jenkov.com/java-concurrency/volatile.html
- happens-before: http://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/happens-before/
- reordering: http://www.infoq.com/cn/articles/java-memory-model-2/
- 正确使用volatile: https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
- 强力推荐!!! 大伙看看这篇 就是要你懂Java中volatile关键字实现原理