0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

介绍下volatile的底层原理

冬至子 来源:并发编程之美 作者:妙啊 2023-06-09 16:17 次阅读

介绍

线程安全的三大特性,原子性、可见性、有序性,这三大特性与我们之前整理的内容息息相关。本篇重点介绍下volatile的底层原理,帮助我们更好的理解java并发包。

一、原子性

提供了互斥访问,同一时刻只能有一个线程来对它进行操作。

1. 原子性-synchronizes

2. 原子性-lock

  • lock属于jdk提供的代码层面上的锁,后面单独总结。

3. 原子性-cas

4. 原子性-对比

  • synchronized:不可中断锁(在作用范围内必须等待执行完),适合竞争不激烈。
  • Lock:可中断锁(unlock),竞争激烈时能保持性能常态。
  • Atomic:竞争激烈时能保持性能常态,比Lock性能好,只能同步一个值。

二、可见性

可见性指的是一个线程对主内存的修改,可以被其他线程及时的观察到。导致共享变量在线程间不可见的原因:

  • 线程交叉执行。
  • 重排序结合线程交叉执行。
  • 共享变量更新后的值没有在工作内存与主内存间及时更新。

1. 可见性-synchronizes

JMM中关于synchronized的内存语意:

  • 进入synchronized块的内存语义是把synchronized块内使用到的变量从线程的工作内存中清除,这样synchronized块内使用到该变量就是直接从主内存中获取。
  • 退出synchronized块的内存语义是把synchronized块内对共享变量的修改刷新到主内存。

2. 可见性-volatile

通过加入内存屏障禁止重排序优化来实现。

JMM中关于volatile的内存语意:

  • 当线程写入volatile变量值时就等价于线程退出synchronized同步块(对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存)。
  • 读取volatile变量值时就相当于进入到同步块(对volatile变量读操作,会在读操作前加入一条load屏障指令,从主内存中读取共享变量)。

当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器中,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值。

下面看一个volatile内存可见性的例子:

public class VolatileCanSeeTest {


    private static volatile boolean initFlag = false;


    public static void main(String[] args) throws InterruptedException {
        new Thread(() - > {
            log.info("init begin");
            while(!initFlag) {
            }


            // if(!initFlag) {while(true){}} // JIT
            log.info("===success===");
        }).start();


        Thread.sleep(1000);


        new Thread(() - > doSomething()).start();
    }


    public static void doSomething() {
        log.info("doSomething begin");
        initFlag = true;
        log.info("doSomething end");
    }
}

查看对应的汇编代码,可以看到使用volatile汇编指令会加上lock前缀指令:

图片

  • rsp】是寄存器的意思,在java内存模型一节中我们介绍了工作内存就是寄存器以及cpu告诉缓存等的一个抽象概念。
  • 这里值得一提的是,重排序只是编译器优化的一种表现,上面这段代码主要是编译器优化导致的。编译器会认为这段循环代码在单线程运行中,initFlag变量不会被改变,从而优化为:
if(!initFlag) {
    while(true){
    }
}

这里结合java内存模型对volatile底层原理进行说明:

图片

  • 这里添加lock前缀指令的意思是当cpu执行引擎处理完共享变量的计算后,通过asign指令将共享变量回写到工作内存中后会立即将该共享变量通过store和write指令回写到主内存,并且给这两个cpu指令加lock指令锁。
  • 同时,结合cpu缓存一致性协议,当共享变量回写主内存时,经过总线触发MESI协议,另其他包含了该共享变量的缓存行置为无效状态,所以其他线程需要从主内存中重新加载该共享变量到自己的工作内存,从而保证了共享变量的内存可见性。同时,结合[cpu缓存一致性协议,当共享变量回写主内存时,经过总线触发MESI协议,另其他包含了该共享变量的缓存行置为无效状态,所以其他线程需要从主内存中重新加载该共享变量到自己的工作内存,从而保证了共享变量的内存可见性。

3. 可见性-对比

  • synchronized:保证可见性和原子性,但可能会导致线程上下文切换和增加重新调度的开销。
  • volatile:只能保证共享变量的可见性,不能解决读-改-写等的原子性问题。
  • 关于共享内存可见性以及JMM详见:java内存模型

三、有序性

一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

1. 有序性-happens-before原则

java内存模型中允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。可以通过volatile关键字来保证一定的有序性,可以通过synchronized、lock保证同一时刻线程顺序执行来保证有序性。另外,java内存模型具备先天的有序性,称为**happens-before **原则:

  • 程序次序规则(保证单线程的有序性,不保证多线程的有序性)

    一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。

  • 锁定规则

    一个unlock操作先行发生于对后面同一个锁的lock操作。

  • volatile变量规则

    对一个变量的写操作先行发生于后面对这个变量的读操作。

  • 传递规则

    如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。

  • 线程启动规则

    Thread对象的start()方法先行发生于此线程的每一个动作。

  • 线程中断规则

    对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

  • 线程终结规则

    线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。

  • 对象终结规则

    一个对象的初始化完成先行发生于他的finalize()方法的开始。

如果两个操作的执行顺序无法从happens-before原则推导出来,那么就无法保证他们的有序性,虚拟机可以随意的对他们重排序。

2. 有序性-synchronizes

首先,可以明确的一点是:synchronized是无法禁止指令重排和处理器优化的。那么他是如何保证的有序性呢?

synchronized保证的有序性是多个线程之间的有序性,即被加锁的内容要按照顺序被多个线程执行。但是其内部的同步代码还是会发生重排序,只不过由于编译器和处理器都遵循as-if-serial语义,所以我们可以认为这些重排序在单线程内部可忽略。

as-if-serial语义的意思指:不管怎么重排序,单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。简单说就是,as-if-serial语义保证了单线程中,不管指令怎么重排,最终的执行结果是不能被改变的。

3. 有序性-volatile

java内存模型允许编译器和处理器对指令重排序提高运行性能,并且只会对不存在数据依赖性的指令重排序。例:

int a = 1;
int b = 2;
int c = a + b;

变量c的值依赖a和b的值,所以重排序后能保证c的操作在a,b之后,但是a,b谁先执行就不一定,这在单线程下不存在问题。下面看一个多线程下指令重排序的例子:

public class VolatileSerialTest {


    private static int x = 0, y = 0;


    public static void main(String[] args) throws InterruptedException{
        Set< String > resultSet = new HashSet<  >();
        Map< String, Integer > resultMap = new HashMap<  >();


        for (int i = 0; i < 1000000; i++) {
            x = 0;
            y = 0;
            resultMap.clear();
            Thread one = new Thread(() - > {
                int a = y;
                x = 1;
                resultMap.put("a", a);
            });


            Thread two = new Thread(() - > {
                int b = x;
                y = 1;
                resultMap.put("b", b);
            });


            one.start();
            two.start();
            one.join();
            two.join();


            resultSet.add("a=" + resultMap.get("a") + "," + "b=" + resultMap.get("b"));
            log.info("ab结果:{}", resultSet);
        }
    }
}

由于指令重排序导致可能出现的结果有:

图片

volatile禁止指令重排序原理:

图片

volatile通过加入内存屏障禁止指令重排序。 编译器会根据volatile/synchronized/final等的语义,在特定的位置插入内存屏障。 当遇到特定的内存屏障指令时,处理器将禁止其对应的重排序,保证屏障前面的操作可以被后面的操作可见。

4. 有序性-对比

  • synchronized是一种锁机制,存在阻塞问题和性能问题,而volatile并不是锁,所以不存在阻塞和性能问题。
  • volatile借助了内存屏障来帮助其解决可见性和有序性问题,而内存屏障的使用还为其带来了一个禁止指令重排的附件功能,所以在有些场景中是可以避免发生指令重排的问题的。

结语

本文总结了线程安全的三大特性,同时文中几乎涉及到了所以之前总结过的知识,在阅读过程中可以参考之前的文章进行理解。至此,我们对并发包基础应该有了完整的认识。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 寄存器
    +关注

    关注

    31

    文章

    5336

    浏览量

    120238
  • JAVA语言
    +关注

    关注

    0

    文章

    138

    浏览量

    20090
  • volatile
    +关注

    关注

    0

    文章

    45

    浏览量

    13023
收藏 人收藏

    评论

    相关推荐

    volatile 和 const

    volatile; 2、多任务环境各任务间共享的标志应该加volatile; 3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;我认为这是
    发表于 06-23 23:20

    介绍计算机底层知识

    的更远,而计算机基础知识又是重中之重。下面,跟随我的脚步,为你介绍计算机底层知识。CPU还不了解 CPU 吗?现在就带你了解一 CPU 是什么CPU 的全称是Central Pr
    发表于 07-26 06:21

    介绍计算机底层知识

    我们每个程序员或许都有一个梦,那就是成为大牛,我们或许都沉浸在各种框架中,以为框架就是一切,以为应用层才是最重要的,你错了。在当今计算机行业中,会应用是基本素质,如果你懂其原理才能让你在行业中走的更远,而计算机基础知识又是重中之重。下面,跟随我的脚步,为你介绍计算机
    发表于 07-28 06:15

    什么是volatile

    volatile06. 附录01. volatile概述volatile是C语言中的一个关键字。将变量定义为volatile就表示告诉编译器这个变量可能会被竟想不到地改变,在这种情况
    发表于 10-28 09:23

    请问一volatile的作用是什么

    请问一volatile的作用是什么?volatile变量有哪些例子呢?
    发表于 11-11 07:49

    AVR-GCC中如何使用volatile关键字

    volatile的字面含义是易变的,那么将一个变量指示为volatile是什么意思呢?是告诉编译器这个变量是易变的?事实上也是如此。在多任务、中断等环境,变量可能被其他的任务改变
    发表于 07-02 17:11 40次下载

    volatile修饰的变量的认识和理解

     谈到volatile,理解原子性和易变性是不同的概念这一点很重要,volatile是轻量级的锁,它只具备可见性,但没有原子特性。如果你将一个域声明为volatile,那么只要对这个域产生了写操作
    发表于 12-01 11:36 5723次阅读
    <b class='flag-5'>volatile</b>修饰的变量的认识和理解

    volatile说到i++的线程安全问题

    中断服务程序中修改的供其它程序检测的变量需要加volatile;多任务环境各任务间共享的标志应该加volatile;存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它
    发表于 12-01 12:01 2990次阅读
    从<b class='flag-5'>volatile</b>说到i++的线程安全问题

    volatile变量定义的意义和该用在哪里

    volatile 影响编译器编译的结果,volatile指出 变量是随时可能发生变化的,与volatile变量有关的运算,不要进行编译优化,以免出错
    发表于 03-07 15:29 3673次阅读
    <b class='flag-5'>volatile</b>变量定义的意义和该用在哪里

    C语言类型修饰符Volatile的使用说明

    ,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。 一般说来,volatile用在如下的几个地方: 1、中断服务程序中修改的供其它程序检测的变量需要加
    的头像 发表于 09-19 10:54 3547次阅读

    如何使用C++语法中的volatile

    volatile volatile int i = 10; volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以
    的头像 发表于 09-09 09:38 1488次阅读

    C++基础语法之volatile、assert()和sizeof()

    volatile volatile int i = 10; volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以
    的头像 发表于 09-09 09:48 1307次阅读

    【嵌入式】C语言中volatile关键字

    volatile06. 附录01. volatile概述volatile是C语言中的一个关键字。将变量定义为volatile就表示告诉编译器这个变量可能会被竟想不到地改变,在这种情况
    发表于 10-21 10:21 6次下载
    【嵌入式】C语言中<b class='flag-5'>volatile</b>关键字

    TiDB底层存储结构LSM树原理介绍

    随着数据量的增大,传统关系型数据库越来越不能满足对于海量数据存储的需求。对于分布式关系型数据库,我们了解其底层存储结构是非常重要的。本文将介绍分布式关系型数据库 TiDB 所采用的底层
    的头像 发表于 01-13 10:00 991次阅读

    volatile的原理

    今天来了解一面试题:你对 volatile 了解多少。要了解 volatile 关键字,就得从 Java 内存模型开始。最后到 volatile 的原理。 一、Java 内存模型 (
    的头像 发表于 10-10 16:33 385次阅读
    <b class='flag-5'>volatile</b>的原理