关于volatile与指令重排序的探讨

写在开头

在之前的学习我们了解到,为了充分利用缓存,提高程序的执行速度,编译器在底层执行的时候,会进行指令重排序的优化操作,但这种优化,在有些时候会带来 有序性 的问题。

那何为有序性呢?我们可以通俗理解为:程序执行的顺序要按照代码的先后顺序。 当然,之前我们还说过发生有序性问题时,我们可以通过给变量添加volatile修饰符进行解决。那么今天,我们继续学习,一起探讨一下volatile与指令重排之间的冤家路窄!

有序性问题

首先,我们来回顾一下之前写的一个关于有序性问题的测试类。

【代码示例1】

int a = 1;(1)
int b = 2;(2)
int c = a + b;(3)

上面的这段代码中,c变量依赖a,b的值,因此,在编译器优化重排时,c肯定会在a,b赋值以后执行,但a,b之间没有依赖关系,可能会发生重排序,但这种重排序即便到了多线程中依旧不会存在问题,因为即便重排对执行结果也无影响。

但有些时候,指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致,我们继续看下面这段代码:

【代码示例2】

public class Test {

    private static int num = 0;
    private static boolean ready = false;
    //禁止指令重排,解决顺序性问题
    //private static volatile boolean ready = false;

    public static class ReadThread extends Thread {

        @Override
        public void run() {

            while (!Thread.currentThread().isInterrupted()) {
                if (ready) {//(1)
                    System.out.println(num + num);//(2)
                }
                System.out.println("读取线程...");
            }
        }
    }

    public static class WriteRead extends Thread {

        @Override
        public void run() {
            num = 2;//(3)
            ready = true;//(4)
            System.out.println("赋值线程...");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReadThread rt = new ReadThread();
        rt.start();

        WriteRead wr = new WriteRead();
        wr.start();

        Thread.sleep(10);
        rt.interrupt();
        System.out.println("rt stop...");
    }
}

我们定义了2个线程,一个用来求和操作,一个用来赋值操作,因为定义的是成员变量,所以代码(1)(2)(3)(4)之间不存在依赖关系,在运行时极可能发生指令重排序,如将(4)在(3)前执行,顺序为(4)(1)(3)(2),这时输出的就是0而不是4,但在很多性能比较好的电脑上,这种重排序情况不易复现。
这时,我们给ready 变量添加一个volatile关键字,就成功的解决问题了。

原因解析

volatile关键字可以禁止指令重排的原因主要有两个!

一、3 个 happens-before 规则的实现

  1. 对一个 volatile 变量的写 happens-before 任意后续对这个 volatile 变量的读;
  2. 一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  3. happens-before 传递性,A happens-before B,B happens-before C,则 A happens-before C。

二、内存屏障
变量声明为 volatile 后,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

内存屏障(Memory Barrier 又称内存栅栏,是一个 CPU 指令),为了实现volatile 内存语义,volatile 变量的写操作,在变量的前面和后面分别插入内存屏障;volatile 变量的读操作是在后面插入两个内存屏障。

具体屏障规则:

  1. 在每个 volatile 写操作的前面插入一个 StoreStore 屏障;
  2. 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障;
  3. 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障;
  4. 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

屏障说明:

  1. StoreStore:禁止之前的普通写和之后的 volatile 写重排序;
  2. StoreLoad:禁止之前的 volatile 写与之后的 volatile 读/写重排序;
  3. LoadLoad:禁止之后所有的普通读操作和之前的 volatile 读重排序;
  4. LoadStore:禁止之后所有的普通写操作和之前的 volatile 读重排序。

OK,知道了这些内容之后,我们再回头看代码示例2中,增加了volatile关键字后的执行顺序,在赋值线程启动后,执行顺序会变成(3)(4)(1)(2),这时打印的结果就为4啦!

volatile为什么不能保证原子性?

我们讲完了volatile修饰符保证可见性与有序性的内容,接下来我们思考另外一个问题,它能够保证原子性吗?为什么?我们依旧通过一段代码去证明一下!

【代码示例3】

public class Test {
    //计数变量
    static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //线程 1 给 count 加 10000
        Thread t1 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count++;
            }
            System.out.println("thread t1 count 加 10000 结束");
        });
        //线程 2 给 count 加 10000
        Thread t2 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count++;
            }
            System.out.println("thread t2 count 加 10000 结束");
        });
        //启动线程 1
        t1.start();
        //启动线程 2
        t2.start();
        //等待线程 1 执行完成
        t1.join();
        //等待线程 2 执行完成
        t2.join();
        //打印 count 变量
        System.out.println(count);
    }
}

我们创建了2个线程,分别对count进行加10000操作,理论上最终输出的结果应该是20000万对吧,但实际并不是,我们看一下真实输出。

输出:

thread t1 count 加 10000 结束
thread t2 count 加 10000 结束
14281

原因:
Java 代码中 的 count++并非原子的,而是一个复合性操作,至少需要三条CPU指令:

  • 指令 1:把变量 count 从内存加载到CPU的寄存器
  • 指令 2:在寄存器中执行 count + 1 操作
  • 指令 3:+1 后的结果写入CPU缓存或内存

即使是单核的 CPU,当线程 1 执行到指令 1 时发生线程切换,线程 2 从内存中读取 count 变量,此时线程 1 和线程 2 中的 count 变量值是相等,都执行完指令 2 和指令 3,写入的 count 的值是相同的。从结果上看,两个线程都进行了 count++,但是 count 的值只增加了 1。这种情况多发生在cpu占用时间较长的线程中,若单线程对count仅增加100,那我们就很难遇到线程的切换,得出的结果也就是200啦。

要想解决也很简单,利用 synchronized、Lock或者AtomicInteger都可以,我们在后面的文章中会聊到的,请继续保持关注哦!

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

在这里插入图片描述
如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/464762.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Gin 框架中前端向后端传值的几种方式介绍

我将为您详细讲解 Gin 框架中前端向后端传值的几种方式&#xff0c;并给出相应的简单例子。Gin 是一个高性能的 Web 框架&#xff0c;用于构建后端服务。在 Web 应用程序中&#xff0c;前端通常需要向后端发送数据&#xff0c;以便后端能够进行处理。以下是几种常见的前端向后端…

数据可视化学习:Matplotlib概述

一、图表的常用设置 1.基本绘图主要函数 (1).matplotlib.pyplot.plot(x,y,format_string,**kwargs) 2.参数说明 (1).x:x轴数据 (2).y:y轴数据 (3).format_string:控制曲线格式的字符串&#xff0c;包括颜色、线条样式和标记样式 (4)**kwargs:键值参数&#xff0c;相当于…

go rabbitmq 操作

go rabbitmq 操作 go 依赖包github.com/streadway/amqp docker快速部署 docker pull rabbitmq:management docker run -d rabbitmq:management # 先跑一个看看监听了哪些端口 docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq #5672 go 程序连接&#x…

TPU浅谈

前言 大家好&#xff0c;我是jiantaoyab&#xff0c;上篇文章讲了FPGA和ASIC&#xff0c;讲解了 FPGA 如何实现通过“软件”来控制“硬件”&#xff0c;以及我们可以进一步把 FPGA 设计出来的电路变成一块 ASIC 芯片。今天我们来看看TPU。大家可以点击这篇文章TPU深入了解TPU。…

操作系统(OS)

文章目录 前言一、操作系统是什么&#xff1f;二、用户对资源的访问三、操作系统是怎么做到管理的&#xff1f; 前言 任何计算机系统都包含一个基本的程序集合&#xff0c;称为操作系统(OS)。冯诺依曼体系结构中的硬件单元提供的功能&#xff0c;这些硬件由操作系统来控制与管…

ChatGPT登陆提示:“Please unblock challenges.cloudflare.com to proceed…”

ChatGPT登陆时提示&#xff1a;“Please unblock challenges.cloudflare.com to proceed”&#xff0c; 说明&#xff1a;请解除对challenges.cloudflare.com的屏蔽以继续 原因及解决方法&#xff1a; 1、出现这个问题&#xff0c;一般都是网络和本地环境问题&#xff0c;可以…

Seata 2.x 系列【7】服务端集成 Nacos 2.x

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Seata 版本 2.0.0 本系列Spring Boot 版本 3.2.0 本系列Spring Cloud 版本 2023.0.0 源码地址&#xff1a;https://gitee.com/pearl-organization/study-seata-demo 文章目录 1. 概述2. 安装 N…

滑动窗口最大值(leetcode hot100)

给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 示例 1&#xff1a; 输入&#xff1a;nums [1,3,-1,-3,5,3,6,7], k 3 输…

mvnd 安装和配置

mvnd 是 maven 的增强工具&#xff0c;在执行速度方面优于 maven 下载安装&#xff1a; https://github.com/apache/maven-mvnd/releases/ 根据不同的系统下载不同的安装包 配置环境变量 Path 新增 mvnd 安装路径下的 bin 目录 E:\maven-mvnd-1.0-m8-m39-windows-amd64\b…

学会Promise,看这里!!!

前言 众所周知&#xff0c;在JavaScript的世界中&#xff0c;代码都是单线程执行的。由于这个原因&#xff0c;JavaScript中的耗时操作&#xff0c;如网络操作、浏览器事件等&#xff0c;都需要异步执行。这也导致在JavaScript中异步操作是非常频繁且常见的。 异步&#xff1a…

B端能用就行,颜值无所谓?你现在还敢说吗,马上轮到工业HMI

在当前的商业环境下&#xff0c;用户体验和界面设计的重要性越来越受到重视&#xff0c;即使是B端用户也希望能够使用界面美观、易于操作的工业HMI系统。 漂亮的设计不仅可以提高用户的工作效率和满意度&#xff0c;还可以提升产品的竞争力和市场份额。因此&#xff0c;即使是…

Java 面试题之框架

1. Spring 是什么 Sping 是包含了众多工具方法的 IOC 容器&#xff0c;IOC是控制反转&#xff0c;说的是对象的创建和销毁的权利都交给 Spring 来管理了, 它本身又具备了存储对象和获取对象的能力. 。 容器&#xff1a;字面意思&#xff0c;用来容纳某种物品的装置。 比如 L…

力扣题目训练(22)

2024年2月15日力扣题目训练 2024年2月15日力扣题目训练563. 二叉树的坡度637. 二叉树的层平均值643. 子数组最大平均数 I304. 二维区域和检索 - 矩阵不可变154. 寻找旋转排序数组中的最小值 II 2024年2月15日力扣题目训练 2024年2月15日第二十二天编程训练&#xff0c;今天主要…

高并发缓存策略大揭秘:面试必备的缓存更新模式解析

在高并发场景中&#xff0c;缓存能抵挡大量数据库查询&#xff0c;减少数据库压力&#xff0c;对于缓存更新通常有以下几种模式可以选择&#xff1a; cache aside read/write through write behind caching cache aside模式 Cache-aside模式是一种常用的用于管理缓存的模…

【linux深入剖析】操作系统与用户之间的接口:自定义简易shell制作全过程

&#x1f341;你好&#xff0c;我是 RO-BERRY &#x1f4d7; 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f384;感谢你的陪伴与支持 &#xff0c;故事既有了开头&#xff0c;就要画上一个完美的句号&#xff0c;让我们一起加油 目录 1.shell2.自定义shell的准…

如何遍历map

小王学习录 前言遍历map集合1. 使用for-each循环遍历 entrySet()2. 使用迭代器遍历 entrySet()3. 通过 keySet() 遍历4. 使用迭代器遍历 keySet()5. 仅遍历 values() 如果只关心map中的值而不关心键&#xff0c;可以遍历 values()&#xff1a;6. 使用流(Streams)进行遍历 总结 …

typeorm导致nestjs通过@Query接收的参数为undefined

依赖版本如下,发现引入typeorm后导致接收不到Query参数,解决办法是将 TypeOrmModule导入语句放到前面就可以了

MT3004·找四边形

题目&#xff1a; 样例输入 4 12 1 2 1 3 1 4 2 1 2 3 2 4 3 1 3 2 3 4 4 1 4 2 4 3 样例输出 12 数据范围 算法设计 涉及的算法 枚举和图论基础 采用邻接矩阵g[N]来存储图&#xff0c;其中vector<ll> g[N]是建立了一个二维的vector 来用sum记录每个点 i 到达点 j…

java集合框架——Map集合概述

前言&#xff1a; 之前接触了单列合集&#xff0c;现在又接触了双列合集。整理下心得&#xff0c;打好基础&#xff0c;daydayup&#xff01;&#xff01; Map集合 Map集合称为双列集合&#xff0c;也被称为“键值对集合”。格式&#xff1a;{key1value1,key2value2...}&#…

网络学习:邻居发现协议NDP

目录 前言&#xff1a; 一、报文内容 二、地址解析----NS/NA 目标的被请求组播IP地址 邻居不可达性检测&#xff1a; 重复地址检测 路由器发现 地址自动配置 默认路由器优先级和路由信息发现 重定向 前言&#xff1a; 邻居发现协议NDP&#xff08;Neighbor Discovery…