Java----volatile关键字

欢迎转载,但请务必在明确位置注明文章出处! http://johnnyshieh.github.io/android/2017/02/25/java-volatile/

在阅读本文前,建议先看下面两篇文章:

Java 内存模型

线程安全之 synchronized 关键字

Java 中 volatile 是轻量级的 synchronized,不会引起线程上下文的切换和调度。虽然 volatile 关键字只能修饰变量,但是要用好它并不容易,下面分析它的原理及注意事项。

1. volatile 关键字的两层语义

  • 保证变量对所有线程的可见性,即一个线程修改了某个值,新值对其他线程来说是立即可见的

  • 禁止指令重排序(Java 5 后才有的)

2. volatile 的可见性保证

要明白可见性语义,需要先了解 Java 内存模型的概念,具体可看Java 内存模型,简单的说就是一个公共变量在每个线程都有线程私有的副本,所以正常情况下,一个线程修改了某个变量的值,只是修改其副本,其他线程不能得知该值的修改。

先看下面这段代码:

boolean count = 0;  // 线程共享变量

// 线程1
count = 1;

// 线程2
System.out.println(count);

线程1先执行 count = 1; ,然后线程2中读取 count 的值时可能为 0 也可能为 1。因为线程2无法知道线程1对 count 变量的修改,但是也不确定的是也不知道该修改什么时候同步到共享内存,不知道何时把共享内存的值更新到线程2的工作内存中。

而 volatile 关键字修饰变量后,每个线程对 volatile 变量的读取都要从共享内存中读,而每个线程对 volatile 变量的写不仅要更新工作内存还要刷新共享内存。而普通变量没法保证 JVM 何时把工作内存的值更新到共享内存,以及把共享内存的值读取到工作内存。所以用 volatile 关键字修饰count 变量后,线程2就可以知道线程1对其的修改了。

volatile 关键字不具备原子性

从上面知道volatile关键字保证了操作的可见性,但是volatile能保证对变量的操作是原子性吗?

看下面这个例子:

public class Test {
    public volatile int count = 0;
     
    public void increase() {
        count ++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i = 0; i < 10; i ++){
            new Thread(){
                public void run() {
                    for(int j = 0;  j< 1000; j ++)
                        test.increase();
                };
            }.start();
        }
         
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.count);
    }
}

上面这段程序的输出结果很多朋友会认为是 10000,因为每个线程自增 1000 次,10线程的话就是 自增 10000 次,结果就是 10000。但是实际上它每次的运行结果都不一定一致,但都是一个小于 10000 的数值。

原因是 volatile 关键字保证了 count 变量的可见性,但是并没有保证 increase() 方法的原子性。自增操作是不具备原子性的,编译成 class 文件的话,它分成三个指令:读取变量的值、进行加 1 操作、写入工作内存(因为是 volatile 变量,所以也会同步到共享内存)。大家可以想象一下这个场景:

某一时刻 count 的值为 10, 线程1对其进行自增操作,读取了值后就被阻塞了,切换到线程2对其进行自增操作,读取到的值为 10,然后进行加 1 操作并写入工作内存,同时同步到共享内存。然后切换回线程1,虽然共享内存中 count 的值为 11, 但是线程1下面的操作没有去读 count 的值,线程1的副本中 count的值还是 10,所以继续自增操作的下面两步后也把 11 更新到共享内存。这样执行两次自增操作,结果 count 只增加了 1。volatile 只能保证每次读取的是最新的值,但是线程没读取时是不知道其他线程修改的值的。

上面的问题的根源就是自增操作不是原子性操作,有下面几种方式把它变为原子性:(1)用 synchronized 修饰 increase() 方法(2)使用 ReentrantLock 把自增操作加锁(3)把 count 改为 AtomicInteger 类型,用 getAndIncrement() 原子操作自增。

3. volatile 禁止指令重排序

volatile 关键字能禁止指令重排序,所以 volatile 能在一定程度上保证有序性。

volatile 关键字禁止指令重排序有两层含义:

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行

  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行

看下单例模式的双重检查锁定写法:

public class Singleton {
    private static volatile Singleton sInstance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if(null == sInstance) {                 //第一次检查
            synchronized(Singleton.class) {
                if(null == sInstance) {         //第二次检查
                    sInstance = new Singleton();
                }
            }
        }
        return sInstance;
    }
}

为什么 sInstance 需要 volatile 关键字修饰,如果不修饰会可能会出现得到未初始化完成的对象,原因就是因为指令重排序。把 sInstance = new Singleton(); 换成字节码伪代码:

memory=allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
sInstance=memory; //3:设置sInstance指向刚分配的内存地址

但是在一些 JIT 编译器上,2 和 3 可能会重排序,变成下面这样:

memory=allocate(); //1:分配对象的内存空间
sInstance=memory; //3:设置sInstance指向刚分配的内存地址
ctorInstance(sInstance); //2:初始化对象

重排序后,sInstance可能会为未调用构造函数的对象,假设线程1执行到上面代码第二步时,还没构造完成,但是 sInstance 非空,切换到线程2调用 getInstance() 方法,因为 sInstance非空就直接返回了,这样就会使用还没构造完成的对象。

但是用 volatile 关键字修饰了 sInstance 变量后,语句3是 volatile 变量的写操作,不能将在对volatile变量访问的语句放在其后面执行,也就不允许语句2放在语句3后面,这样就能避免返回还未初始化完成的对象。

4. volatile 的实现原理

下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。


参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值