Java volatile 整理

2019-02-07  本文已影响9人  SlowGO

一、volatile 特性

多线程环境下,每个线程都有自己的一个工作内存,对于共享变量,读时是先从主内存加载到工作内存,写时是从工作内存写回主内存。

这个工作内存就相当于一个线程级别的缓存,与其他缓存一样,存在一致性问题,比如一个共享变量的值在一个线程的工作内存中被修改了,那么什么时候同步回主内存呢?这个是没有明确规定的,这就导致了问题,例如多个线程都在自己的工作内存中修改共享变量的值,而主内存中的值是旧的,各个线程也不知道其他线程对变量的改动,这就是不可见问题。

volatile可以保证:

从而,volatile 解决了可见性问题,每次读写都是直接关联主内存的,这是 volatile 的首个重要特性。还有一个重要特性是防止指令重排序,具体的后面再说。

小结一下volatile的特性:

  1. 保证了共享变量在多线程下的可见性。
  2. 防止指令重排序。

二、volatile 是怎么保证可见性的?

1. 什么是可见性问题?

多线程应用中,为了性能,每个线程都会把共享变量从主内存拷贝到自己的工作内存中,多cpu计算机中每个线程运行在不同的cpu中,每个线程就把共享变量拷贝到自己的cpu cache中。

image

对于普通变量,JVM什么时候将其从主内存读到 cpu cache 中?以及什么时候从 cpu cache 写回主内存?这些都是没有保证的。

public class SharedObject {

    public int counter = 0;

}

如这段代码,假设线程1对counter执行增加操作,线程1和线程2都会时不时的读取 counter,线程1的操作结果什么时候写回主内存是没谱的,就会发生下图中的情况:

image

可见性问题:一个线程修改了变量值,由于还没有写回主内存,导致其他线程看不到最新值,也就是一个线程的更新对其他线程不可见。

2. volatile 对可见性的保证

变量声明了 volatile 之后,所有对其写后都会立即写回主内存,所有对其的读操作都会直接从主内存中读。

大概原理:

多处理器下,会实现一个缓存一致性协议,每个处理器通过分析在总线上传播的数据来检查自己缓存的值是否过期了,所以当 volatile 变量值写回到主内存后,其他处理器会发现自己缓存行对应的内存地址被修改了,就会将当前缓存置为无效状态,所以当再次操作这个变量时就得从主内存重新读取到缓存,从而拿到最新值。

3. 全可见性保证

事实上,volatile 对可加性的保证已经超过了 volatile 变量本身,还遵循如下原则:

示例代码:

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;
    }
}

udpate()写了3个变量,其中只有 daysvolatile

days写入后,所有可见变量(years、months)也会写入主内存。

读取的示例代码:

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时,years、months也会从主内存中读取。

三、volatile 是怎么防止重排序的?

为了提高性能,JVM和CPU允许对指令的顺序进行重排,只要保证整体语义和结果是正确的。

例如:

int a = 1;
int b = 2;

a++;
b++;

可以重拍为:

int a = 1;
a++;

int b = 2;
b++;

看下面的代码:

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,那么years months也都会写入主内存。但如果发生了重拍的话呢,例如:

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

days的写入被排在了第一行,虽然写入的同时也会把years months的值写入主内存,但之后又修改了years months的值,这时他俩的最新值就不会被马上写入主内存了,这就与重排序之前的代码意义不同了。

为了解决重排序问题,volatile 给出了“happens-before”保证:

参考:http://tutorials.jenkov.com/java-concurrency/volatile.html

上一篇 下一篇

猜你喜欢

热点阅读