Java Volatile Keyword - 译文

Java Volatile Keyword

  并发是程序界的量子物理,然而volatile又是量子物理中薛定谔的猫。本篇文章试图系统的梳理一下java中的Volatile关键字。这篇译文可能帮助你更好的理解volatile关键字。
   使用volatile关键字是解决同步问题的一种有效手段。 java volatile关键字预示着这个变量始终是“存储进入了主存”。更精确的表述就是每一次读一个volatile变量,都会从主存读取,而不是CPU的缓存。一样的道理,每次写一个volatile变量,都是写回主存,而不只仅是CPU的缓存。
javascript


  事实上,JAVA5的volatile关键字不仅是保证了每次从主存读写数据。下面将着重介绍volatile关键字的特性。

  Java 保证volatile关键字保证变量的改变对各个线程是可见的。这看起来有点抽象,不过将紧接着说明这一点。
咱们知道,每个线程都有本身的线程栈。多线程在操做非volatile变量的时候,都会从主存拷贝变量值到本身的栈内存中间,而后再操做变量。在多个线程的状况下,若是一个线程修改了变量值还未回写到主内存,另外一个线程读取的就是一个旧的值,这样会出现问题,由于读到的变量不是最新的。实际上,在多核CPU中间,因为每一个CPU都有本身的缓存,一样会存在主存与CPU缓存之间数据不一致的状况。所以,在C语言中,也有volatile关键字。(译者注:实际上,若是在CPU的层面知足volatile特性,那么线程栈就必定知足。由于从volatile语义来说,jvm线程每次只从主存读写volatile变量,而主存的volatile变量又在CPU层面知足volatile语义)
想象一种这样的状况,有两个或者更多的线程访问一个共享对象,这个共享对象包括了一个counter变量:
public class SharedObject {java

public int counter = 0;复制代码

}
  再想象一下,只有线程1对counter变量加一,可是线程一和线程2倒是同时读到这个变量。缓存

  若是这个contouer变量没有被声明为volatile。
就不能保证counter变量从cpu缓存回写到主存。这就意味着counter变量在cpu缓存中的值与主存中值不一致。多线程

  这就是所谓的线程不能看到变量最新值的问题。由于另一个线程并无及时将变量写回到主存。这样一个线程的人更新对其余线程是不可见的。并发

  经过声明counter变量是一个volatile变量,这样全部counter变量的更改就会被当即写入主存。一样,对counter变量的读也从主存里面读。下面是如何声明一个volatile变量:app

public class SharedObject {

    public volatile int counter = 0;

}复制代码

  经过声明volatile变量就保证了对其余线程写的可见性。jvm

java volatile的happen-before保证

  java5中的volatile关键字不仅是保证从主存中读写数据,实际上,volatile还保证以下的状况:性能

  • 若是线程a写一个volatile变量,随后线程b读取这个变量,而后全部的变量在线程a写以前可见,全部的变量也在b读以后对线程b可见了。(译者注:volatile有两个语义:可见性与读写原子性。a在写变量的过程当中,b是没法读取的。由于CPU会锁定这块内存区域的缓存并回写到内存。此时B才能够读取,若是A在写的过程当中B能够读取,那么线程B读取的是脏数据。i++之因此没法用volatile保证原子性。是由于volatile仅仅保证读取加锁,赋值加锁,而对于中间的加1操做是不会加锁的。线程B若是在这个期间读取值,那确定会是脏数据。)

  读和写volatile变量的指令没法被JVM重排序(JVM为了提升性能能够重排序一些指令,只要程序的行为与排序前同样)可是volatile变量却没法重排序,也就是volatile变量的读和写没法被打乱在其余变量中间。不论是什么指令,老是在volatile变量读写以后发生。优化

  下面将会详细的解释这一点。ui

  当一个线程写一个volatile变量,而后不只仅是volatile变量自己自身写入到主存。全部其余的在写volatile变量以前也会被刷入主存。当一个线程读volatile变量的时候,它也会从主存读取其余变量。(译者注:注意是全部的变量。每次在写入volatile变量的时候,线程栈里面的全部的共享变量都将刷回主存,而不只仅是在volatile变量声明以前的变量)

  看下面这个例子,sharedObject.counter是一个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变量,而后再写入volatile变量,这个时候非volatile变量sharedObject.nonVolatile 也会被写入主存。

  当线程B开始读一个volatile变量sharedObject.counter,而后全部的sharedObject.nonVolatile以及

  sharedObject.counter都会从主存读取。这个时候sharedObject.nonVolatile值与线程A中的值是同样的。

  开发者可使用这种扩展的可视性来优化线程之间的可视性:不是对每一个变量都声明为volatile变量,而是只须要声明其中一部分变量为volatile。下面是Exchanger类,就利用了上述的原则:

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方法。这个Exchanger可以在合理使用volatile关键字的状况下工做的很好。只要线程A只调用put方法,线程b只调用take方法。

  然而,JVM是能够对指令进行优化的。若是JVM对指令优化,打乱了顺序,会出现什么样的效果呢?下面这段代码多是执行的顺序之一:

while(hasNewObject) {
    //wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;复制代码

  注意到volatile变量hasNewObject如今在object被设置以前执行了。这个对于JVM来讲看起来好像是合法的,由于这两个值的写入指令相互是没有依赖的,JVM能够对它们重排序。

  然而,重排指令有可能影响到object变量的可见性。首先,线程B看见在线程A尚未对object赋值以前就看见了hasNewObject是一个true变量,这样操做线程B读取了一个空值。其次,这甚至不能保证object变量会被及时的写入到主存。(固然,下一次线程A更改volatile变量的时候就会被刷进主存)

  为了阻止上面的任何一种状况发生,volatile保证了“happens 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 可能重排序前三个指令。只要他们所有在volatile写入指令前发生(他们必须在volatile写入前所有执行)

  相似的,JVM可能重排序最后三个指令。只要volatile变量写操做在它们前发生。最后这三个指令都不能被排在volatile变量写指令前面。

  这就是最基本的javavolatile变量的happens before原则。

volatile一般是不够的。

  即便是volatile关键字保证了读写都是从主存读取,然而仍然有写状况不能简单的使用variable变量来解决。在早先讲到的例子中,当线程1写入一个变量counter这个volatile以后,就能保证线程2读到这个最新的值。

  事实上,若是线程在写volatile变量并不依赖于这个volatile以前的值,那么在写的过程当中,主存中仍然是当前的值。

  而后一个线程开始读这个volatile变量。那么这个线程读到的值就是旧的值,可见性就是不正确的。这就会形成读变量和写变量之间的竞争。volatile关键字只是保证了下一次读取的是最新的变量,可是在另一个变量写入的过程当中,读到的值仍然是旧的。(译者注:若是是多个CPU先写后读,在写的过程当中实际上会发出信号,告知其缓存已经失效,因此并不会存在这种状况;至于先读后写,读取一个旧的值的时候要在代码里保证并不会引起任何错误。)。

相关文章
相关标签/搜索