5分钟搞懂volatile,让面试官问到不想问

42 阅读5分钟
                             **引言**
2W1H原则及结论先行原则,从易到难一文过关,让你能给不懂的人也讲清楚
what 是什么
why 为什么这么设计
How 怎么用
先给出结论再解释

1.volatile 是什么?

volatile是关键字,用于修饰变量。

2.修饰变量后有什么作用?

volatile的特性核心分为3个:可见性,有序性,不具备原子性。

  • 保证可见性,更新共享资源的时候,直接同步回主内存

原本的共享资源的交互是,从主内存中获取到共享资源,到工作内存中操作变更会有一份暂存的副本,最终同步给主内存,这样就会有一个gap的间隙。

ex:

private boolean flag = false;

while(tag){
  xxx
}

1.线程A执行
2.线程B执行
3.线程A更新tag为ture;
4.线程B是无法感知到的,因为线程B获取的是工作内存中的历史旧flag,导致的结果就是线程B一直死循环

如果是
private volatile boolean flag = false;
修饰的 线程A更新为ture 线程B也会同步感知到
  • 保证有序性,volatile修饰的资源可以避免cpu的指令重排,提供内存屏障的作用(内存屏障就更深一层了,不是代码层面的处理了)

2.1面试题:单例模式中的volatile有什么作用

首先,为什么把这个面试题放在这里(高频),考察volatile的理解。结合上述的结论一起来讲

public class Singleton {
    // 关键:instance用volatile修饰
    private static volatile Singleton instance;

    private Singleton() {} // 私有构造

    public static Singleton getInstance() {
        // 第一次检查:避免不必要的锁竞争
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二次检查:防止多线程同时进入外层if后重复创建
                if (instance == null) {
                    instance = new Singleton(); // 核心:new对象的指令可能重排序
                }
            }
        }
        return instance;
    }
}

上述代码【核心】

instance = new Singleton();

这一步jvm中的操作不是原子性的,主要被拆分成3步。

1.为对象分配内存空间

2.创建对象,并为对象赋值

3.将这个对象的内存地址指向 instance

那么正常逻辑 是执行 1->2->3,但是因为cpu的指令重排,你的执行顺序可能是 1->3->2。

这样导致的问题是:

线程A,B同时调用getInstance() 方法

线程A执行到创建对象的 1->3,还未执行(2)创建对象及赋值
这时线程B获取instance,发现instance已经有了内存地址的引用,那就会直接返回,导致线程B使用的是未完成初始化的对象

那么问题出现了,我们怎么解决?
通过volatile关键字,可以禁用cpu的指令重排,使得我们的instance 按照123的顺序初始化完成(回到开头说的特性,这就是有序性的体现)。

> **总结**
> 通过volatile关键字可以避免其他线程获取到未完成实例化的对象。
> 顺序==重排序→半初始化对象→volatile 禁止重排序 + 保证可见性
  • 不是原子性
private volatile int count = 0;
public void test(){
 count++; //分为3步, count = count + 1  查+加+赋值
          //1.count查询 count = 0
          //2.count + 1 
          //3.count = 赋值
}

1.线程A执行(1)
2.同时线程B执行(1)都查询到count 是 0
3.线程A执行加,执行赋值 这时count = 1
4.线程B执行加,执行赋值 这时 count 又赋值为了1
5.最终count结果是1,实际上加了2次,应该是2

3.volatile 和synchronized的区别

特性volatilesynchronized
作用范围变量方法,代码块
原子性非原子原子性,同一时间只有一个线程执行
可见性保证保证
有序性有序 内存屏障有序 临界处理
性能轻量级,无锁竞争,开销低开销高,重量级,有线程阻塞风险
结论适用于轻量级的处理,比如单例模式场景下保证线程安全可以解决原子性,保证线程安全性

4. volatile 能替代 synchronized 吗?什么场景下使用 volatile?

不能,结合3,volatile 是非原子性的操作,在并发场景下会有线程安全的问题。synchronized 是保证原子性的,所以不能替换。
那什么场景需要volatile,源码里哪些用到了volatile呢?

结合特性考虑

1.因为是保证了可见性,其他线程也能看到变量的变动,那就可以通过这个方式来控制**状态标记位(线程启停、开关控制)**

ex:

// 合理:仅写线程修改,读线程判断,无复合操作
private volatile boolean isRunning = true;

public void stop() {
    isRunning = false; // 单一写
}

public void work() {
    while (isRunning) { // 单一读
        // 业务逻辑
    }
}

2.避免指令重排,如单例场景

5.volatile既然可以有这些特性,那所有的字段都加?

肯定不合理。
还是按照特性来说
1.因为可见性,所以内存的更新是实时的,正常的业务逻辑当中肯定有一些场景的属性是不需要立马回写到主内存当中的,这样会增加内存的消耗。
2.因为禁止指令重排,这是cpu的一种优化机制提高cpu处理性能,如果你都加了这个禁止指令重排后,性能利用率会大幅下降。

所以综上所述,只有部分场景需要这两个特性的属性时才会用到