腾讯Java二面:volatile原理分析,你能答出来吗

介绍

使用 volatile 修饰的变量是线程共享的全局变量,是轻量级锁的一种表现形式,因为不需要线程上线文切换和调度这些操作,效率杠杠的,但是不能保证原子性,并发场景下要小心使用,比如:多个线程同时执行 i++ 是有问题的。

volatile 的 Demo 代码:

/**
 * 单例模式(懒汉式)
 * @date:2020 年 7 月 14 日 上午 9:48:24
 */
public class Singleton {
    public static volatile Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {     //代码 1
            synchronized (instance) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Singleton 对象是使用 volatile 修饰,所有线程都可见此对象,即有可能被多个线程同时访问此对象,比如有 A 和 B 两条线程同时进入代码 1,如果 B 线程获取锁进行对象初始化,A 线程自旋等待拿锁,B 线程完成初始化对象后释放锁,然后 A 线程获取锁后判断对象是否为 null,为了避免再次初始化对象节约了系统开销,所以此处必须使用双重校验 null。

特性及原理

可见性

任意一个线程修改了 volatile 修饰的变量,其他线程可以马上识别到最新值。实现可见性的原理如下。

步骤 1:修改本地内存,强制刷回主内存。

[

步骤 2:强制让其他线程的工作内存失效过期。

步骤 3:其他线程重新从主内存加载最新值。

单个读/写具有原子性

单个 volatile 变量的读/写(比如 vl=l)具有原子性,复合操作(比如 i++)不具有原子性,Demo 代码如下:

public class VolatileFeaturesA {
    private volatile long vol = 0L;

    /**
     * 单个读具有原子性
     * @date:2020 年 7 月 14 日 下午 5:02:38
     */
    public long get() {
        return vol;
    }

    /**
     * 单个写具有原子性
     * @date:2020 年 7 月 14 日 下午 5:01:49
     */
    public void set(long l) {
        vol = l;
    }

    /**
     * 复合(多个)读和写不具有原子性
     * @date:2020 年 7 月 14 日 下午 5:02:24
     */
    public void getAndAdd() {
        vol++;
    }

}

互斥性

同一时刻只允许一个线程操作 volatile 变量,volatile 修饰的变量在不加锁的场景下也能实现有锁的效果,类似于互斥锁。上面的 VolatileFeaturesA.java 和下面的 VolatileFeaturesB.java 两个类实现的功能是一样的(除了 getAndAdd 方法)。

public class VolatileFeaturesB {
    long vol = 0L;

    /**
     * 普通写操作
     * @date:2020 年 7 月 14 日 下午 8:18:34
     * @param l
     */
    public synchronized void set(long l) {  
        vol = l;
    }

    /**
     * 加 1 操作
     * @author songjinzhou
     * @date:2020 年 7 月 14 日 下午 8:28:25
     */
    public void getAndAdd() {
        long temp = get();
        temp += 1L;
        set(temp);
    }

    /**
     * 普通读操作
     * @date:2020 年 7 月 14 日 下午 8:33:00
     * @return
     */
    public synchronized long get() {
        return vol;
    }
}

部分有序性

JVM 是使用内存屏障来禁止指令重排,从而达到部分有序性效果,看看下面的 Demo 代码分析自然明白为什么只是部分有序:

//a、b 是普通变量,flag 是 volatile 变量
int a = 1;            //代码 1
int b = 2;            //代码 2
boolean flag = true;  //代码 3
int a = 3;            //代码 4
int b = 4;            //代码 5

PS:因为 flag 变量是使用 volatile 修饰,则在进行指令重排序时,不会把代码 3 放到代码 1 和代码 2 前面,也不会把代码 3 放到代码 4 或者代码 5 后面。但是指令重排时代码 1 和代码 2 顺序、代码 4 和代码 5 的顺序不在禁止重排范围内,比如:代码 2 可能会被移到代码 1 之前。

内存屏障类型分为四类。

1. LoadLoadBarriers

指令示例:LoadA —> Loadload —> LoadB

此屏障可以保证 LoadB 和后续读指令都可以读到 LoadA 指令加载的数据,即读操作 LoadA 肯定比 LoadB 先执行。

2. StoreStoreBarriers

指令示例:StoreA —> StoreStore —> StoreB

此屏障可以保证 StoreB 和后续写指令可以操作 StoreA 指令执行后的数据,即写操作 StoreA 肯定比 StoreB 先执行。

3. LoadStoreBarriers

指令示例: LoadA —> LoadStore —> StoreB

此屏障可以保证 StoreB 和后续写指令可以读到 LoadA 指令加载的数据,即读操作 LoadA 肯定比写操作 StoreB 先执行。

4. StoreLoadBarriers

指令示例:StoreA —> StoreLoad —> LoadB

此屏障可以保证 LoadB 和后续读指令都可以读到 StoreA 指令执行后的数据,即写操作 StoreA 肯定比读操作 LoadB 先执行。

实现有序性的原理:

如果属性使用了 volatile 修饰,在编译的时候会在该属性的前或后插入上面介绍的 4 类内存屏障来禁止指令重排,比如:

  • 在 volatile 写操作的前面插入 StoreStoreBarriers 保证 volatile 写操作之前的普通读写操作执行完毕后再执行 volatile 写操作。
  • 在 volatile 写操作的后面插入 StoreLoadBarriers 保证 volatile 写操作后的数据刷新到主内存,保证之后的 volatile 读写操作能使用最新数据(主内存)。
  • 在 volatile 读操作的后面插入 LoadLoadBarriers 和 LoadStoreBarriers 保证 volatile 读写操作之后的普通读写操作先把线程本地的变量置为无效,再把主内存的共享变量更新到本地内存,之后都使用本地内存变量。

volatile 读操作内存屏障:

volatile 写操作内存屏障:

3.使用场景

状态标志,比如布尔类型状态标志,作为完成某个重要事件的标识,此标识不能依赖其他任何变量,Demo 代码如下:

public class Flag {
    //任务是否完成标志,true:已完成,false:未完成
    volatile boolean finishFlag;

    public void finish() {
        finishFlag = true;
    }

    public void doTask() { 
        while (!finishFlag) { 
            //keep do task
        }
    }
}

一次性安全发布,比如:著名的 double-checked-locking,demo 代码上面已贴出。

开销较低的读,比如:计算器,Demo 代码如下。

/**
 * 计数器
 */
public class Counter {
    private volatile int value;
    //读操作无需加锁,减少同步开销提交性能,使用 volatile 修饰保证读操作的可见性,每次都可以读到最新值 
    public int getValue() {
        return value; 
    }
    //写操作使用 synchronized 加锁,保证原子性
    public synchronized int increment() {
        return value++;
    }
}

最后

觉得不错的小伙伴记得转发关注哦,后续会持续更新精选技术文章!

你可能感兴趣的:(腾讯Java二面:volatile原理分析,你能答出来吗)