一文讲解volatile关键字的各种使用


Java内存模型

JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进⾏特别的注意的。⽽在当前的 Java 内存模型下,线程可以把变量保存本地内存(⽐如机器的寄存器)中,⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另外⼀个线程还继续使⽤它在寄存器中的变量值的拷⻉,造成数据的不⼀致。
在这里插入图片描述
在这里插入图片描述

要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是不稳定的,每次使⽤它都到主存中进⾏读取。

在这里插入图片描述


保证共享变量可见性的两种方式:

(1)加锁

  • 线程获得锁
  • 清空当前工作内存
  • 从主内存中拷贝共享变量最新值到工作内存成为副本
  • 执行代码,如果更新了共享变量的值,刷新回主内存中
  • 释放锁

(2)使用volatile修饰共享变量

  • 线程a从主内存读取共享变量到对应的工作内存
  • 对共享变量进行更改
  • 线程b读取共享变量的值到对应的工作内存
  • 线程a将修改后的值刷新到主内存,失效其他线程对共享变量的副本
  • 线程b对共享变量进行操作时,发现已经失效,重新从主内存读取最新值,放入到对应工作内存
volatile的特性

(1)volatile不能保证原子性

(2)volatile保证数据的可见性

(2)使用volatile禁止指令重排序

保障volatile原子性操作的方式

(1)加锁机制

(2)使用原子类

比如AtomicInteger,在底层它是使用了volatile来修饰变量,保证了数据的可见性,同时基于CAS机制,保证了数据的原子性。

volatile在单例模式中的应用

懒汉式(volatile双重检查模式)
点击进入_8种单例模式的实现方式

public class Singleton6 {
    //2.提供静态变量保存实例对象
    private volatile static Singleton6 INSTANCE;

    //1.私有化构造器
    private Singleton6(){}

    //3.提供获取对象的方法
    public static  Singleton6 getInstance(){
        //第一重检查:针对很多个线程同时想要创建对象的情况
        if(INSTANCE == null){//线程C
            //同步代码块锁定
            synchronized (Singleton6.class){
                //第二重锁检查(针对比如A,B两个线程都为null,第一个线程创建完对象,第二个等待锁的线程拿到锁的情况)
                if(INSTANCE == null){
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}

问题1:原子操作

再经过第二重锁检查后,开始创建对象,但实际上创建对象的过程分几个步骤:

  1. 分配内存空间
  2. 调用构造器,初始化实例
  3. 返回地址引用

此时new Singleton6()可能是一个非原子操作,编译器可能会重排序【只是在内存中开辟一片储存区域就直接返回给引用,还未进行赋值操作】,但此时线程C想要创建对象,即经过第一重检查,但此时对象只是一个半成品【不为null,但未进行初始化】,当此线程C拿着这个对象进行数据操作时,就会发生空指针异常

问题2:可见性

假如前面都执行正常,也没有发生重排序操作,此时线程A创建了实例对象,但是还未通刷新到主内存中,此时线程B想要创建实例对象,即过第一重检查【但此时主内存中的INSTANCE仍为null】,那么线程B又将再自己的工作内存中创建一个实例对象,这样就创建了多个实例对象。

针对上面两个问题,对静态实例变量使用volatile修饰,它的两个作用,第一个禁止重排序【创建对象时不会打乱顺序】,第二个保证可见性【每次一个线程对共享变量进行了修改后,刷新更新后的值到主内存中,同时使其他线程工作内存中的变量副本失效,这样当线程C再经过第一重检查时,会从主内存中重新取得共享变量的值,发现INSTANCE不为null,不进入代码】

volatile的使用场景
(1)纯赋值操作,不适合做a++操作

如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以使用volatile代替synchronized或者代替原子变量,因为赋值操作本身是原子性的,而volatile又保证了可见性,所以可以保证线程安全。

(2)触发器

volatile作为刷新之前变量的触发器。我们可以将某个变量使用volatile修饰,其他线程一旦发现该变量修改的值后,触发获取到的该变量之前的操作都将是最新的且可见的。【基于volatile的可见性和禁止重排序以及happen-before规则实现】

volatile与synchronized的区别

volatile只能修饰实例变量和类变量,而可以synchronized修饰实例方法和代码块。

volatile保证数据的可见性,但是不能保证数据的原子性,而synchronized两者都能保证【是一种互斥机制】。

volatile可以禁止指令重排序,可以检查使用单例双重检查中对象创建时执行代码的乱序问题。

volatile可以看做是轻量版的synchronizedvolatile不保证原子性,但是如果对一个共享变量进行多线程的纯赋值操作,那么就可以使用volatile代替synchronized,因为赋值本身是原子性的,而volatile保证了可见性,即可以保证是线程安全的。

小结:volatile的使用

(1)volatile适用以下场景:某个属性被多个线程共享,其中一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;作为触发器,实现轻量级同步。

(2)volatile属性的读写都是无锁的,它不能代替synchronized,因为它没有提供原子性和互斥行,正因为无锁,不需要花费时间在获取锁和释放锁上,低成本。

(3)volatile只能修饰属性,这样编译器就不会对这个属性做指令重排序。

(4)volatile保证了可见性,任何一个线程对共享变量的修改对其他线程可见,即volatile修饰的属性不会被缓存,始终从主存中读取。

(5)volatile提供了happen-before规则保证对volatile变量v的写入happen-before所有其他线程对v的读操作。

(6)volatile使得longdouble的赋值是原子的。

(7)volatile在单例双重检查中实现可见性禁止指令重排序,从而保证线程安全性。

获取参考资料 提取码:jizh

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值