volatile关键字

2019-07-07  本文已影响0人  归来依旧少女

一、volatile保证内存可见性

jvm规定所有变量数据需要存放在主内存中,同时各线程又有自己的工作内存(用来做高速缓存)。数据由于cpu与内存速度上的差异,所以线程工作的时候,不是直接操作主内存的数据,而是将数据复制主内存的一份副本到线程自己的工作内存中,操作完再更新主内存。


线程工作.png

如图,Thread A将i从主内存复制到自己的变量中,Thread B也复制了一份i。这时候Thread A对i进行了修改,i修改后的值为5,去更新主内存i的值。但是Thread B并没有再次读取主内存的值,而是直接使用自己工作内存的值i=1,这就导致数据不一致了。
那么如何解决呢?就需要使用volatile了。volatile修饰的变量保证内存可见性,所谓内存可见性就是当一个变量被修改时会立即更新到主内存中。i修改为5了,同时其他线程中的变量i失效了,必须从主内存中重新读取,这时候读取的就是新值5了。

二、volatile特性

  1. 内存可见性。
  2. 防止指令重排。

内存可见性已经讲过了,下面讲一下这个防止指令重排。

int a=1 ; // 1
int b=2 ; // 2
int c= a+b ; // 3

上面这段代码我们想象中执行的顺序时1-》2-》3。但其实步骤1和步骤2的关系不大,所以jvm优化后可能执行的顺序是2-》1-》3。jvm指令优化是在不改变最终结果的情况下进行指令重排,在单线程情况下是没有关系的,因为不影响最终结果。而多线程的情况下,可能会发生意想不到的结果。用volatile修饰后,就会按顺序执行了。

三、volatile使用场景

3.1 内存可见性,比如状态标识量

public class Sale {
    private volatile boolean openFlag;
    
    public void setOpenFlag(boolean flag){
        this.openFlag = flag;
    }
    
    public void saleGoods(){
        if(openFlag){
            //开店营业
            ...
        }else{
            //关店调整
            ...
        }
    }
}

openFlag用volatile修饰,保证获取到的都是最新值。

3.2 防止指令重排,这个比较经典的就是单例模式

/**
 * <h1>单例模式</h1>
 */
public class Singleton {
    private volatile static Singleton singleton;

    /**
     * 构造器私有化
     */
    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

singleton用volatile修饰,防止指令重排。因为 singleton = new Singleton()会分解为三个操作:
1.分配内存
2.初始化
3.变量指向分配的内存地址
如果不用volatile修饰,因为步骤2和3没有关系,所以可能jvm执行的顺序是1-》3-》2 。所以线程A在执行后singleton已经指向新分配的内存地址,但还没执行初始化这一步。线程B这时候进来了,判断singleton不为null,就返回了,但这时候singleton是未初始化的,这就有问题了。

参考文档:
https://mp.weixin.qq.com/s/qXFuilWNOTJGrV9rKQERaA

上一篇下一篇

猜你喜欢

热点阅读