Basic Of Concurrency(七: volatile关键字)

Java中volatile关键字用于标记Java变量“始终存储在主存中”.这意味着每次都是从主存中读取volatile修饰的变量,且每次对volatile修饰的变量的更改都会写回到主存中,而不是cpu缓存.html

事实上,自java5以后,volitle关键字保证的不仅是始终在主存中读取和修改volatile修饰的变量.java

变量可见性问题

volatile保证了线程间对共享变量的修改是可见的.缓存

在多线程应用中,对于没有volatile修饰的变量,每一个线程在执行过程当中都会从主存中拷贝一份变量的副本到cpu缓存中.假设计算机中拥有多个cpu,每一个线程可能在不一样的cpu上执行,即每一个线程极可能将变量加载到不一样的cpu缓存中.如图所示:多线程

没有volatile修饰将不能保证JVM什么时候从主存中读取变量或什么时候将变量写回主存.这将致使若干问题的发生.app

假设两个或更多的线程访问一个包含有counter变量的共享对象,声明以下:post

public class SharedObject{
    public int counter = 0;
}
复制代码

想象一下,当只有线程1对counter进行累加计算,但线程1和线程2在日后的时间会不定时的从主存中加载变量counter.性能

若是没有将变量counter修饰为volatile将不能保证对变量counter的修改会在什么时候写回主存.这意味着不一样cpu缓存中counter变量的值可能与主存中不同.如图所示:this

问题在于线程1对counter变量的修改对于线程2不可能见.这种一个线程对共享变量的修改对于另外一个线程不可见的问题,咱们称之为"可见性"问题.spa

volatile对于可见性的保障

Java中 volatile关键字用于解决可见性问题.若将counter修饰为volatile,那么全部对于counter的修改会被当即写回到主存中,且限制counter只能从主存中读取.线程

public class SharedObject{
    public volatile int counter = 0;
}
复制代码

将变量修饰为volatile保证了变量修改对其余线程的可见性.

上文中说起线程1对counter的修改,线程2对counter的读取可以经过volatile来保证线程1对counter变量的修改对于线程2可见.

然而,若是线程1和2同时累加counter变量,此时仅仅将变量修饰为volatile是不够的.详情在下文会说起.

volatile对可见性的充分保障

事实上,volatile对于可见性保障不只仅局限于volatile修饰的变量自己.可见性保障内容以下所示:

  • 若是线程1修改volatile修饰的变量,紧接着线程2读取一样volatile修饰的变量,那么线程1对修改volatile变量以前其余变量的修改都会对线程2可见.
  • 若是线程1读取一个volatile修饰的变量,那么volatile修饰变量以后用到的其余变量都会强制从主存中读取以保证全部变量对于线程1可见.

代码实例:

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以前的yeas 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,紧接着连同参与计算的months和years也一块儿从主存中读取.所以你能够保障上面days months和years的读取都是最新的.

指令重排序带来的挑战

JVM和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 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;
    }
}
复制代码

一旦update()方法更新days变量,那么对于years和months的更新也会被写回到主存中,若JVM进行重排序:

public void update(int years, int months, int days){
    this.days   = days;
    this.months = months;
    this.years  = years;
}
复制代码

当对days变量进行修改时,months和years的修改也会被写回主存,但此次对days的修改是在months和years以前,所以对于months和years的最新修改不会对其余线程可见.重排序后的语义已经发生改变.

volatile关于Happens-Before的保障

针对指令重排序的挑战,volatile给出了"happens-before"保障,用于补充可见性保障.happens-before保障的内容以下所示:

  • 若对于其余变量的读写原顺序是在写volatile修饰变量以前进行的,不能被重排序为以后进行.保证了写volatile变量以前对其余变量的读写操做正常的发生.相反,容许对于其余变量的读写原顺序是在写volatile修饰变量以后的,被重排序为以前进行.
  • 若对于其余变量的读写原顺序是在读volatile变量以后的,不能被重排序为以前进行.保证了读volatile变量以后对其余变量的读写操做正常的发生.相反,容许对于其余变量的读写原顺序是在读volatile修饰变量以前的,被重排序为以后进行.

happens-before保证了volatile可见性保障的强制执行.

volatile并不老是足够的

尽管volatile保障了volatile修饰的变量老是从主存中读取和写回主存,但仍是有些状况即便将变量修饰为volatile也不能知足.

以前的状况是线程1对于volatile变量的修改老是对于线程2可见.

在多线程下,若是产生的新值并不依赖主存中的旧值(不须要使用旧值来推导出新值),那么即便两个线程同时更新主存中volatile修饰的变量值也不会有问题.

当一个线程产生的新值须要依赖旧值时,那么仅仅用volatile修饰共享变量来保障可见性是不够的.当一个线程在读取主存中volatile修饰的共享变量以前,此时有两个线程同时从主存中加载相同的volatile修饰的变量,同时进行更新且写回主存时会产生竞态条件,此时两个线程对旧值的更新会互相覆盖.那么以后线程从主存中读取的数值多是错误的.

这种状况下,当两个线程同时累加相同的counter变量时,用volatile修饰变量已经不能知足了.以下所示:

当线程1从主存加载counter到cpu缓存中,此时counter为0,对counter进行累加以后counter变为1,此时线程1尚未将counter写回主存,线程2一样将主存中counter加载到cpu缓存中进行累加操做.此时线程2也尚未将counter写回主存.

实际上线程1和线程2是同时进行的.而主存中counter变量的预期结果应该为2,但如图所示两个线程在各自缓存中的值为1,而在主存中的值为0.即便两个线程将各自缓存中的值写回主存也是错误的.

volatile什么状况才是足够的?

两个线程同时对一个共享变量进行读写操做时,使用volatile修饰已经不能知足状况.你须要使用synchronized来保障变量读写操做的原子性.使用volatile并不能同步线程的读写操做.这种状况下只能使用synchronized关键字来修饰临界区代码.

除了synchronized,你还能够选择java.util.concurrent包中提供的原子数据类型.如AtomicLongAtomicRefrerence等.

在其余状况下,若是只有一个线程对volatile修饰的变量进行读写操做,其余线程只进行读操做,那么volatile是足够保障可见性的,若没有volatile修饰,那就不能保障了.

volatile关键字在32bit和64上的变量可用.

volatile实践建议

对于volatile修饰变量的读写可以被强制在主存中进行(从主存中读取,写回主存).直接在主存中读写的性能消耗远大于在cpu缓存中读写.volatile可以在特定状况有效防止指令重排序.因此应该谨慎使用volatile,只有在真正须要保障变量可见性的状况下使用.

该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: 同步代码块
下一篇: ThreadLocal

相关文章
相关标签/搜索