JMM之volatile关键字详解

1、概要

在JMM规范下有三大特性分别是:可见性、原子性、有序性。而被volatile关键字修饰的共享变量拥有三大特性的两大特性分别是:可见性和有序性。

为什么被volatile修饰的变量就可以保证变量的可见性和有序性呢?为啥不能保证原子性?我们带着这两个疑问展开深入分析。

2、volatile内存语义

当写一个被volatile修饰的变量时,JMM会把该线程对应的本地内存中共享变量值立即刷新回主内存中。

当读一个被volatile修饰的变量时,JMM会把该线程对应的本地变量内存设置为无效,重新回到主内存中读取最新共享变量。

总结:volatile写内存语义是执行刷新到主内存中,读的内存语义是直接从主内存中读取。

什么叫本地变量内存?

什么叫主内存?

大白话:我们声明的所有变量都存储在主内存中,每个线程都有一份本地变量内存(线程私有),在线程读取变量时,需要从自己的本地内存获取,如果本地内存不存在则需要先去主内存拷贝一份变量到本地变量内存中,然后再从本地内存获取。在线程修改变量时,也是先修改本地内存中的变量值,然后在某个时机将本地内存值刷新回主内存(问题就出在这里,什么时候刷新回主内存时间不确定,其他线程不知道,获取的可能是脏数据)这是JMM规范下的正常变量读取和更新规则。

有了volatile,则会打破这个规则,读取时每次都是从主内存拷贝到本地内存,然后读取本地内存值。更改时先更改本地内存值立马刷新会主内存。

大家是不是会发现,不管是读取还是写入都需要 经过 本地内存,假如在经过本地内存过程中,又被改了或者被读取了,是不是也就不能保证数据可见和有序呀。这就要说到JMM四大内存屏障了,有了这四大内存屏障,就可以保证万无一失。

3、内存屏障

java中每一行代码经过编译后都会被分解成一条或者多条指令,例如下面两行代码分解成3条指令。

代码:

int i = 0;
i++;

编译后指令:

0 iconst_0
1 istore_1
2 iinc 1 by 1

在不同的操作系统上为了最大限度提升性能,编译器和处理器可能对指令进行重排序,也就是说第一行代码可能不是先执行。

不存在数据依赖关系:可以重排序;

存在数据依赖关系:禁止重排序;

但是重排后的指令绝对不能改变原有的串行语义(单线程执行结果不受重排序影响)

什么是内存屏障(也称内存栅栏,屏障指令等):是一类同步屏障指令,是CPU或者编译器在对内存随机访问操作中的一个同步点,使得此点之前所有的读写操作都执行后才开始执行此点之后的操作,避免重排序。 

大白话:现在有10个人,要过安检,安检门口工作人员要求先进入5个人,等5个人安检完以后后续5人才可以进入。安检门口工作人员就是内存屏障。这里面前前五个人进入后可以不分先后检查,同样后五个人进入也不分先后检查。但是后五个人不能排在前五个人前面。

内存屏障其实就是一种JVM指令,Java内存模型的重排序规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了java内存模型中的可见性和有序性(禁止重排序),但是volatile无法保证原子性。

有了内存屏障,内存屏障之前的写操作都要回写到主内存中,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。

3.1、内存屏障分类

大分类分别是: 

写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(线程本地内存)中的数据同步到主内存中,也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。

读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行,也就是说在Load屏障指令之后就能保证后面的读取数据指令一定能够读取到最新的数据。

细分就是4类,分别是:

屏障类型 示例 说明
LoadLoad Load1;LoadLoad;Load2 保证Load1的读取操作在Load2及后续读取操作之前执行
StoreStore Store1;StoreStore;Store2 在Store2及其后的写操作之前,保证Store1的写操作已刷新到主内存
LoadStore Load;LoadStore;Store 在Store及其后的写操作执行前,保证Load的读操作已读取结束
StoreLoad Store;StoreLoad;Load 保证Store的写操作已刷新到主内存之后,Load及其后续读操作才能执行

4、volatile使用场景

4.1、状态标志,判断业务是否结束


    static volatile Boolean stop = Boolean.FALSE;

    public static void main(String[] args) {
        new Thread(() -> {
            while (!stop) {
                System.out.println("----执行内容");
            }
        }, "t1").start();

        //执行结束,将stop标识设置为true,通知线程1结束工作
        new Thread(() -> stop = true, "t2").start();

    }

4.2、开销较低的读、写策略

当读的频率远大于写的频率是,结合使用内部锁和volatile变量来减少同步的开销。

理由:利用volatile可见性保证读取操作的可见性,利用synchronized保证复合操作的原子性

    /**
     * 通过volatile 保证可见性
     */
    private volatile int value;

    
    public int getValue() {
        return value;
    }

    /**
     * 通过synchronized 保证原子操作
     */
    public synchronized void setValue() {
        value++;
    }

4.3、DCL双端锁的发布

package com.lc.test03;

/**
 * @author liuchao
 * @date 2023/4/12
 */
public class ThreadUtil {

    /**
     * 私有构造方法
     */
    private ThreadUtil() {
    }

    /**
     * 通过volatile声明,实现线程安全的延迟初始化
     */
    private static volatile ThreadUtil instance;

    /**
     * 两次加锁 DCL(Double Check Lock)
     *
     * @return
     */
    public static ThreadUtil getInstance() {
        if (null != instance) {
            return instance;
        }
        synchronized (ThreadUtil.class) {
            if (null != instance) {
                return instance;
            }
            instance = new ThreadUtil();
        }
        return instance;
    }
}

猜你喜欢

转载自blog.csdn.net/u011837804/article/details/130167699