13.Java的Volatile关键字

这个Java的volatile关键字是用来标示一个Java变量作为“正在被存储在主内存的”。更加准确地说意味着,一个volatile变量的每一次读取都是从计算机的主内存中读取,而不是从CPU缓存中,并且对于一个volatile变量的每一次写将会写到主内存中,而不只是写入到CPU缓存中。

事实上,自从Java5开始这个volatile关键字不只是保证变量写到主内存,而且还从主内存中读取。我将会在接下来的部分中解释。

Java的volatile关键字的可见性的保证

这个Java的volatile关键字保证横跨线程中对于变量改变的可见性。这个可能听起来有点抽象,让我们详细阐述一下。

在一个多线程的应用中,线程操作在非volatile变量上,每一个线程在工作的时候可能会从主内存拷贝变量进入到CPU缓存中,因为性能原因。如果你的计算机包含不只是一个CPU,那每一个线程可能会运行在不同的CPU中。那就意味着,每一个线程就会拷贝变量进入到不同的CPU的CPU缓存中去。如下图所示:


使用非volatile的变量,这里不能保证JVM什么时间从主内存读取数据进入到CPU缓存中,或者写数据从CPU缓存进入主内存中。这个可能就会引起在下面部分将会解释的几个问题。

想象一个场景,两个或者更多的线程访问一个包含声明了一个counter变量的一个共享对象,像下面这样:

public class SharedObject {

    public int counter = 0;

}

也想象一下,只有线程1增加counter变量,但是线程1和线程2偶尔会读取这个counter变量。

如果这个counter变量没有声明为volatile,那就不能保证这个counter变量什么时间从CPU缓存中写回到主内存中。这个就意味着,这个在CPU缓存中的counter变量跟在主内存中的值是不同的。如下图所示:


线程不能看到这个变量的最新的值的这个问题是因为它还没有被其他的线程写回到主内存中,别称之为”可见性”问题。一个线程的更新对于其他的线程是不可见的。

通过声明这个counter变量为volatile,对于counter这个变量的所有写入将会立刻写回到主内存中。同时,对于counter变量的所有读取将会直接从主内存中读取。这里有一个counter变量怎么样声明为volatile:

public class SharedObject {

    public volatile int counter = 0;

}

声明一个变量为volatile,因此可以保证针对这个变量的写对于其他线程的可见性。

这个Java的volatile关键字保证了前后顺序

自从Java5以来,这个volatile关键字不只是保证了变量从主内存的读和写。实际上,volatile关键字还保证了这个:

  • 如果线程A写向一个volatile变量,以及线程B随后读取这个变量,然后在写这个volatile变量之前,所有的变量对于线程A是可见的,在它已经读取这个volatile变量之后也会对线程B可见的。
  • 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;

因为在写这个volatile的counter之前,线程A写了非volatile得nonVolatile变量,然后当线程A写这个counter(volatile变量)的时候,非volatile得变量也被写回到了主内存中。
因为线程B开始读取counter这个volatile变量,然后这个counter变量和nonVolatile变量都会被线程B从主内存读取到CPU缓存中。这个时候线程B也会看到被线程A写的这个nonVolatile变量。
开发者可能使用这个扩展的可见性保证来优化线程之间变量的可见性。代替声明每一个变量为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方法获取这个对象。这个类可以工作的很好通过使用一个volatile变量(没有使用synchronized锁),只要只是线程A调用put方法,线程B调用take方法。
然而,JVM可能重排序Java指令去优化性能,如果JVM可以做这个没有改变这个重排序的指令。如果JVM改变了put方法和take方法内部的读和写的顺序将会发生什么呢?如果put方法真的像下面这样执行:
while(hasNewObject) {
    //wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;

注意这个volatile变量的写是在新的对象被真实赋值之前执行的。对于JVM这个可能看起来是完全正确的。这两个写的执行的值不会互相依赖。
然而,重排序这个执行的执行将会危害object变量的可见性。首先,线程B可能在线程A确定的写一个新的值给object变量之前看到hasNewObject这个值设为true了。第二,现在甚至不能保证对于object的新的值是否会写回到主内存中。
为了阻止上面所说的那种场景发生,这个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可能会重排序前面的三个指令,只要他们中的所有在volatile写执行发生前(他们必须在volatile写指令发生前执行)。
类似的,JVM可能重排序最后3个指令,只要volatile写指令在他们之前发生。最后三个指令在volatile写指令之前都不会被重排序。
那个基本上就是Java的volatile保证先行发生的含义了。
volatile关键字不总是足够的
甚至如果volatile关键字保证了volatile变量的所有读取都是从主内存中读取,以及所有的写也是直接写入到主内存中,但是这里仍然有些场景声明volatile是不够的。
在更早解释的场景中,只有线程1写这个共享的counter变量,声明这个counter变量为volatile是足够确保线程2总是看到最新写的值。
事实上,如果在写这个变量的新的值不依赖它之前的值得情况下,甚至多个线程写这个共享的volatile变量,仍然有正确的值存储在主内存中。换句话说,如果一个线程写一个值到这个共享的volatile变量值中首先不需要读取它的值去计算它的下一个值。
如果一个线程需要首先去读取这个volatile变量的值,并且建立在这个值的基础上去生成一个新的值,那么这个volatile变量对于保证正确的可见性就不够了。在读这个volatile变量和写新的值之间的短时间间隔,出现了一个竞态条件,在这里多个线程可能会读取到volatile变量的相同的值生成一个新的值,并且当写回到主内存中的时候,会互相覆盖彼此的值。
多个线程增加相同的值得这个场景,正好一个volatile变量不够的。下面的部分将会详细解析这个场景。
想象下,如果线程1读取值为0的共享变量counter进入到CPU缓存中,增加1并且没有把改变的值写回到主内存中。线程2读取相同的counter变量从主内存中进入到CPU缓存中,这个值仍然为0。线程2也是加1,并且也没有写入到主内存中。这个场景如下图所示:

线程1和线程2现在是不同步的。这个共享变量的真实值应该是2,但是每一个线程在他们的CPU缓存中都为1,并且在主内存中的值仍然是0.它是混乱的。甚至如果线程最后写他们的值进入主内存中,这个值是错误的。
什么时候volatile是足够的
正如我前面提到的,如果两个线程都在读和写一个共享的变量,然后使用volatile关键字是不够的。你需要使用一个synchronized在这种场景去保证这个变量的读和写是原子性的。读或者写一个volatile变量不会堵塞正在读或者写的线程。因为这个发生,你必须使用synchronized关键字在临界区域周围。
作为一个synchronized锁可选择的,你也可以使用在java.util.concurrent包中的许多原子数据类型中的一个。例如,这个AtomicLong或者AtomicReference或者是其他中的一个。
假如只有一个线程读和写这个volatile变量的值,其他的线程只是读取这个变量,然后读的这个线程就会保证看到最新的值了。不使用这个volatile变量,这个就不能保证。
volatile关键字的性能考虑
volatile变量的读和写引起了这个变量将会读或者写到主内存。从主内存读或者写到主内存比访问CPU缓存有更大的消耗。访问volatile变量也会阻止指令重排序,这也是一个标准的性能增加技术。因此,你应该只有当你真正的需要变量的强烈可见性的时候应该使用volatile变量。


翻译地址:http://tutorials.jenkov.com/java-concurrency/volatile.html

你可能感兴趣的:(java,并发)