Java 中 volatile 关键字解析

volatile 解析

volatile 是Java 虚拟机提供的最轻量级的同步机制,它有两个功能:

  • 保证此变量对所有线程的可见性,可见性的意思是当一个线程修改这个变量的值,新值对于其他线程来说是可以立即得知的。即 Java 内存模型确保所有线程看到这个变量的值是一致的
  • 禁止指令重排序优化。普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。这就是Java内存模型中描述的”线程内表现为串行的语义“。

为什么会不一致?

  • 从硬件角度看:在多处理器中每个处理器有各自的L1,L2等高速缓存。为了提高运行速度,刚用过的变量值会暂时存储在L1/L2高速缓存而不是立刻写回内存中(多处理器用相同的内存),这样如果一个处理器修改了某个变量而只写入自己的L1/L2高速缓存中,其他处理器就看不到这个修改值,这样就会造成不一致。
  • JMM的角度看:JMM 规定了每个线程有自己的工作内存,如果工作内存不及时写回主内存进行同步,则各个线程相同的变量可能存在不同值。

什么是指令重排序?

指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。但并不是说指令任意重排,CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。比如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值减去3,这时指令1指令2是有依赖的,它们之间的顺序不能重排,但指令3可以重排到指令1、 2之前或者中间,只要保证CPU执行后面依赖到A、 B值的操作时能获取到正确的A和B值即可。

解决方法

就是把变量声明为volatile类型,如果对声明了volatile的变量进行写操作,会向处理器发送一条Lock前缀的指令,作用:

  • 将这个变量所在的缓存行的数据写回主内存
  • Lock 操作相当于一个内存屏障,指令重排序时不能把Lock后面的指令重排序到内存屏障之前的位置。

Lock前缀的指令在多核处理器下会引发两件事情:

  • 将当前处理器缓存行的数据写回到系统内存。
  • 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。(缓存一致性协议

使用 volatile优化:一般L1/L2/L3的高速缓存行是64个字节宽,不支持部分填充缓存行,即64字节的数据同时锁定,同时写回内存,同时在其他CPU的高速缓存中失效。将两个经常分别读写的变量分到不同缓存行中,不至于同时被失效,影响效率。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性。
  • 运算结果并不依赖变量的当前值(比如 setValue() 方法不依赖当前值,++ 操作符则依赖当前值),或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。
volatile 用法

Java 里面的运算并非原子操作,volatile变量的运算不能保证原子性

public class VolatileTest {
    public static volatile int race = 0;

    public static void increase() {
        race++;
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        //等待所有累加线程都结束
        while (Thread.activeCount() > 1)
            Thread.yield();
        System.out.println(race);
    }
}

这里的 race++; 并不是原子操作,volatile只保证了变量的可见性,所以这里是错误的用法。

volatile 的使用场景

volatile boolean shutdownRequested;

    public void shutdown() {
        shutdownRequested = true;
    }

    public void doWork() {
        while (!shutdownRequested) {
        //do stuff
        }
    }

这里 shutdown() 方法被调用时,能保证所有线程中执行的 doWork() 方法都立即停下来。

volatile 的使用场景二

    Map configOptions;
    char[]configText;
    //此变量必须定义为volatile
    volatile boolean initialized=false;
    
    //假设以下代码在线程A中执行
    //模拟读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可用
    configOptions=new HashMap();
    configText=readConfigFile(fileName);
    processConfigOptions(configText,configOptions);
    initialized=true;

    //假设以下代码在线程B中执行
    //等待initialized为true,代表线程A已经把配置信息初始化完成
    while(!initialized){
        sleep();
    }
    //使用线程A中初始化好的配置信息
    doSomethingWithConfig();

如果定义 initialized 变量时没有使用 volatile 修饰,就可能会由于指令重排序的优化,导致位于线程A中最后一句的代码 initialized=true 被提前执行,这样在线程B中使用配置信息的代码就可能出现错误,而 volatile 关键字则可以避免此类情况的发生。

参考自《深入理解Java虚拟机》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值