java的volatile关键字用来标注变量是存在主存中的。更精确的说,每一次读volatile变量都会从计算机主存中读取,而不是从CPU缓存中读取,每一次写入volatile变量都会最后写到主存中去,而不仅仅是CPU缓存。实际上,自从java5开始,volatile关键字不仅仅保证了变量的读和写都是在主存中发生的,我会在下边的章节中解释
java volatile保证可见性
java volatile关键字保证了线程间的变量改变的可见性,这听起来有点抽象,我们来详细描述一下。在多线程的应用中,多个线程在非volatile变量上操作,出于性能上的考虑,当在这些变量上边工作的时候每一个线程都会copy主存中变量的一个副本进入CPU缓存中,如果你的计算机包含了多个的CPU,每个线程或许会运行在不同的CPU上,也就意味着每个线程会把变量拷贝到不同的CPU缓存中,下边这个图可以解释这个问题
如果没有volatile变量的时候,当JVM在主存读数据到内存或者从CPU到主存中写数据的时候就没有保证,这会导致很多的问题,在下边的章节中我会讲到这个问题。想象一个场景,两个或者多个线程访问一个包含counter变量的共享对象,如下:
public class SharedObject {
public int counter = 0;
}
再想一下,只有线程1增加了counter变量,但是线程1和线程2时不时的要读取counter变量。如果counter变量没有声明为volatile,当在CPU中读回counter变量的时候就没有保证,也就意味着CPU中counter变量的值或许和主存中的值不一样,下边的图展示了这种情况。
线程没有看到变量最新的值是因为还没有被另一个线程写回到主存中去,这就是可见性的问题,一个线程的更新对于另一个线程是不可见的。通过声明counter变量为volatile,所有对counter变量的写入都会立刻写回到主存中去,同样的,所有的对counter变量的读取都是直接从主存中读取,按照下边的方式声明:
public class SharedObject {
public volatile int counter = 0;
}
声明变量为volatile就保证了对其他线程的可见性。
java volatile happens-before 保证
自从java5 volatile关键字不仅仅保证了从主存中读和写变量,实际上volatile关键字保证了这个:
1、如果线程A写进一个volatile关键字声明的变量中, 线程B紧接着读取了同一个volatile变量,那么在写入volatile变量之前所有的变量都是可见的,当读完volatile变量之后对线程B都是可见的。
2、对volatile变量读和写的步骤不能够被JVM锁记录(JVM或许会因为性能的原因记录步骤,只要JVM程序中没有什么改变)前和后的步骤可以被记录,但是volatile的读和写不能被和这些步骤混淆。不管什么样的步骤读和写volatile关键字都会保证发生
我们需要对这些话有一个更深的了解。
当一个线程写入一个volatile变量时,不仅仅是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之前写入非volatile变量
sharedObject.nonvolatile。当线程A写sharedObject.counter时,这两个变量都会写入到主存中去。由于线程B开始读取volatile变量sharedObject.counter,那么变量sharedObject.counter和非volatile变量sharedObject.nonvolatile两个变量都从主存中读入到缓存中。到线程B读取变量sharedObject.nonvolatile时,它将看到线程A写入的值。开发人员或许使用扩展的可见性来保证优化变量的可见性。与每一个变量都声明为volatile相反,只有一个或者少量的被声明为volatile,看下边的代码
public class Exchanger {
private Object object = null;
private volatile hasNewObject = false;
public void put(Object newObject) {
while(hasNewObject) {
//wait - do not overwrite existing new object
}
object = newObject;
hasNewObject = true; //volatile write
}
public Object take(){
while(!hasNewObject){ //volatile read
//wait - don't take old object (or null)
}
Object obj = object;
hasNewObject = false; //volatile write
return obj;
}
}
线程A或许通过调用put方法随时把对象存入,线程B随时同时take方法获取对象。这个exchange使用volatile变量而没有使用synchronize块可以工作的很好,只要只有线程A调用了put线程B调用了take。但是,JVM或许记住了java的步骤来优化性能,如果JVM在不改变语义的情况下记录步骤。如果JVM改变了两个方法的读和写的顺序会发生什么呢,如果put方法实际上这么执行呢?
while(hasNewObject) {
//wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;
注意:在新的对象真正的设置之前,写入volatile变量hasNewObject开始执行。对于JVM来说这可以完全没有任何问题,两个写步骤的值没有互相依赖彼此。但是记录执行的步骤会损害Object变量的可见性。首先,在线程A实际把新的值写入到object变量之前线程B或许看到hasNewObject设置为true,其次,新的写入到object值什么时候刷回到内存中时没有任何的保证。
为了阻止这种情况的发生,这个volatile关键字是“happens before guarantee”。这个happens before guarantee保证了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或许记录了前三个步骤,只要他们是在volatile关键字之前发生的。同样的,只要volatile写发生在他们之前JVM或许会记录最后三个步骤。这就是基本的意思。
volatile并总是足够的
即使volatile关键字保证了从主存中读取所有的volatile变量,对所有的volatile变量的写都直接写入到主存中,但是依然有一些情况是volatile关键字处理不了的。在这种情况下,我们之前也说过,只有线程1写入共享变量counter可以确保线程2看到最新的值。实际上多线程可以写入到共享volatile变量中去,并且依旧可以有正确的值存储在主存中,如果新的值不依赖于其前值的话。换句话说,线程写入共享volatile变量时,不需要先知道其原来的值来计算其后来的值。
当线程需要先读取其前值,来计算其后值的时候,volatile关键字并不能保证其正确可见性。在向volatile变量读取值和写入新的值之间有一个短暂的时间空隙,在多线程之间创造了竞争条件,多线程或许读取了相同的volatile值,为变量产生了新的值,当写回主存的时候,重写了彼此的值。这种情况下,多线程同样增加了counter,volatile关键字不足以处理。下一节会详细介绍这种情况。
想一下,如果线程1读取了共享的值为0的counter变量进入CPU缓存,把它的值增加1并且不把这个改变之后得值写回主存中,线程2依然把值为0的变量从主存中读取出来到它自己的缓存中去,线程2同样也把值增加了1,并且也没有写回到主存中去,下边的图展示了这种情况
现在线程1和线程2并不是同步的,counter的真是的值应该是2,但是每个线程中在CPU缓存中的值为1,主存中依然为0,这全乱了,即使把值写回到内存中,它的值也是错的。
什么时候只使用volatile就够了
就像我之前提到的,如果两个线程都读和写一个共享的变量仅仅使用volatile关键字是不够的。你需要使用synchronize关键字,那种情况下,可以保证读和写的原子性。读和写一个volatile变量不会阻塞线程的读和写,对于这种情况,你必须使用synchronize在关键区的周围。
作为使用synchronize块的一个替代你可以使用在java.util.concurrent.package包中的很多原子的数据类型。例如AtomicLong等等。在这种情况下,只有一个线程读和写共享变量,其他的线程只能读这个变量,然后读的线程能够保证看到最新的值,没有把变量声明为volatile就无法保障这一点。这个volatile关键字可以在32和64位上工作。
volatile的性能考虑
读和写volatile变量引起变量读和写到主存中去,从主存中读和写数据要比从CPU缓存中读和写数据慢的多,访问volatile变量同样也会阻止记录步骤(这是性能加强的一个技术),因此,你使用volatile关键字的时候,应该是你必须要使用的时候。
翻译的并不好,读的不顺的同学,可以读下边的原文或者在评论中给出意见
原文地址:http://tutorials.jenkov.com/java-concurrency/volatile.html