volatile的作用

volatile是用来修饰成员变量的,它的作用有两个:保证变量的修改在多线程之间的可见性、禁止指令重排。

volatile的内存可见性保证

在java内存模型中,变量都是保存在主内存中的,主内存是一块儿公共的内存区域,所有的线程都可以访问它,但是如果线程想要对变量做出修改,就只能将这个变量从主内存copy到自己的工作内存中,去修改自己工作内存中的变量副本,因为线程是无法直接读写主内存的,而工作内存是被线程私有的,任何线程都无法直接去访问其他线程的工作内存,所以当线程对一个变量做出修改之后,这个修改要被写回主内存之后,才有可能被其他线程读到,而如果我们没有用volatile关键字修饰变量,那变量的这个修改并不是被立即写回主内存的,工作内存写主内存是有时机的。

工作内存写主内存的时机包括:

1、当退出synchronized块儿并释放锁时;

2、当把一个从执行引擎接收到的值赋给工作内存中的变量时;

3、当线程结束执行时。

所以,非volatile变量被某个线程修改之后,这个修改并不会被立即写回主内存,即便是修改被写回了主内存,这个修改也不一定会被其他线程读到,因为线程在读取共享变量时,并非每一次都去读主内存中的新值,线程在初次读取共享变量时,会将其copy到自己的工作内存中,后续的读取读的是工作内存中的变量副本,而变量副本的更新也是有时机的。

工作内存的刷新时机包括:

1、当线程获取到锁,进入synchronized块儿时;

2、当线程完成了一次上下文切换重新执行时:比如,线程之前因为执行了Thread.sleep或者锁对象的wait方法而进入阻塞状态,后续苏醒或者被唤醒之后重新被cpu调度而执行时,会将主内存中变量的新值刷新到工作内存中;

3、当执行此线程的cpu内核空闲时

4、当然,线程在初次读取变量时,因为工作内存中还没有保存副本,所以也要从主内存往工作内存中刷新。

总之,非volatile变量被修改之后,这个修改并不会被立即写回主内存,线程读取变量时,也不是每次读取都将主内存的新值刷新到工作内存,也就是,非volatile变量的修改在多线程之间并不是立即可见的。而我们的业务需求有的时候是要求保证变量修改在多线程之间的立即可见性的,这个时候就可以用volatile来解决问题了。

volatile变量在被修改之后,会被立即写回主内存,并且线程在读取volatile变量时,每次读取都会去主内存刷新新值到工作内存,因此,volatile变量的修改在多线程之间是立即可见的。

volatile的禁止指令重排

volatile的第二个作用就是禁止指令重排,为了提高程序的执行性能,编译器会对指令进行重排序优化,指令重排有两个前提:

1、指令重排不能影响单线程的执行结果

2、存在依赖关系的指令不允许重排

虽然指令重排不会改变单线程的执行结果,但是会破坏多线程的执行语义。所以在多线程场景下,指令重排有时候会带来线程安全问题,最经典的例子就是双重判断加锁的懒汉式单例模式:

java 复制代码
public class Singleton {
    private static volatile Singleton instance;
    private Singleton(){

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

当我们没有用volatile关键字去修饰成员变量instance时,在第一个执行getInstance方法的线程获取到锁,进入执行instance = new Singleton();时,这虽然看似只是一行代码,但在编译后它包含着三个操作:1、为将要创建的对象实例分配内存空间;2、初始化这个对象;3、将内存地址赋给instance变量。在没有用volatile关键字去修饰instance时,这三个操作将有可能被重排序,而排序后的结果可能变成了132,也就是,先为对象分配内存,然后就将内存地址赋给了instance变量,最后才初始化对象,而在第二步将内存地址赋给instance变量之后,这个instance变量就不为空了,这时如果有另外一个线程来执行getInstance方法,那么它在经过判断之后得到Instance != null,那么这个线程就会拿到instance所指向的内存中的对象,而这个对象可能只被初始化了一半,而用一个只被初始化了一半的对象来执行接下来的业务操作,可能会得到一个错误的执行结果。所以有的时候我们需要去禁止指令重排,而禁止的方案就是在变量上加volatile关键字。

volatile禁止指令重排的底层原理是内存屏障,通过在需要保证执行顺序的指令间插入内存屏障能禁止指令重新排序,内存屏障保证的是:当执行到屏障前面的指令时,屏障后面的指令还未被执行;而当执行到屏障后面的指令时,屏障前面的指令已经执行完成。

volatile无法保证原子性

虽然volatile能保证变量修改在多线程之间的可见性,以及通过禁止指令重排保证了指令执行的有序性,但是无法保证变量操作的原子性,因此它无法做出线程安全保证。比如,当我们用volatile来修饰一个int类型的变量时,这个int变量的++操作仍然是线程不安全的,若要保证共享变量的线程安全,还是得通过加锁的方式来实现。

相关推荐
better_liang2 小时前
每日Java面试场景题知识点之-消息队列MQ核心场景与实战
java·面试·kafka·消息队列·rabbitmq·rocketmq·mq
小江的记录本2 小时前
【JVM虚拟机】垃圾回收GC:四种引用类型:强引用、软引用、弱引用、虚引用(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试
小马爱打代码3 小时前
Spring源码 第四篇:Spring 5 源码深度拆解:AOP 全流程核心原理
java·后端·spring
deepin_sir3 小时前
10 - 函数
开发语言·python
better_liang3 小时前
每日Java面试场景题知识点之-SpringBoot启动流程
java·面试·springboot·源码解析·启动流程
RyFit3 小时前
Java + AI 实战:Spring AI 从入门到企业级落地
java·人工智能·spring
z落落3 小时前
C#String字符串
开发语言·c#·php
猫头虎-前端技术3 小时前
JS 作用域与闭包:从变量提升到闭包陷阱的超详细解析
开发语言·javascript·云计算·bootstrap·ecmascript·openstack·perl
枫叶林FYL4 小时前
项目十:事件溯源仓储管理系统(WMS)仿真实现
开发语言·python
繁华落尽,倾城殇?4 小时前
[C++11] : atomic,nullptr,default/delete,enum class
开发语言·c++·c++11·nullptr·atomic·enum class·default/delete