并发bug之源(二)-有序性

news2025/2/4 1:15:18

什么是有序性?

简单来说,假设你写了下面的程序:

java

int a = 1;
int b = 2;
System.out.println(a);
System.out.println(b);

但经过编译器/CPU优化(指令重排序,和编程语言无关)后可能就变成了这样:

java

int b = 2; 
int a = 1;
System.out.println(a);
System.out.println(b);

当然上面例子这种情况,就算调整了代码顺序,也没有任何影响。但实际工作过程中,这种擅自优化,并不总是没有问题的,在多线程情况下,有时候就会给我们的程序中埋下一个隐藏的bug。

如何证明指令重排序的存在?

我说有指令重排序就有啊,那不得拿出证据来?

这个证明有点复杂,我先写一个程序,跑起来看结果,然后再解释:

java

public class DisOrder {

    private static int a, b, x, y = 0;

    public static void main(String[] args) throws InterruptedException {
        for (long i = 0; i < Long.MAX_VALUE; i++) {
            a = 0;b = 0;x = 0;y = 0;
            CountDownLatch cdl = new CountDownLatch(2);
            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
                cdl.countDown();
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
                cdl.countDown();
            });
            t1.start();
            t2.start();
            cdl.await();
            
            if (x == 0 && y == 0) {
                System.out.println("第" + i + "次循环时, (" + x + "," + y + ")");
                break;
            }
        }
    }

}
  • 这个程序需要等一会,执行结果:

我来解释下这个程序在干什么,程序中有四个成员变量 a, b, x, y ,初始都是0。

然后执行一个无限循环,循环中启动两个线程,两个线程分别去修改 a, b, x, y 四个变量,按顺序一共有四行代码:

none

a = 1;
x = b;
b = 1;
y = a;
  • 每次修改完成后,判断下x和y是否都为0,是则打印 x, y 并停止循环,否则重新循环,并将四个变量归零。

OK,现在我们先来简单推理下。

假设程序严格按照代码的顺序去执行,那么两个线程修改完成后,a, b, x, y 的值有哪些可能呢?

我猜你懒得推理,直接说结论吧:

结果一共有6种可能性,一种为x=0,y=1,一种为x=1,y=0,另外四种都为x=1,y=1。

可以发现,没有任何情况的结果是x=0,y=0的。

但是,我们从上面程序实际执行结果可以看到,循环终止了,也打印出了当第35239次循环时,x=0,y=0

那么也就是说,必然发生了上面6种可能性以外的其他情况。大家可以再简单推理下,发生什么情况会导致 x=0,y=0 呢?

我们发现只有上面这2种情况,会导致x=0,y=0。

而从这两种情况可以发现,代码执行的顺序,和我们写的顺序发生了交换,第一个线程里原本是a = 1;x = b;,在这两种情况里,都变成了x = b;a = 1;,第二个线程里也是如此。

由此可以证明,指令重排序的存在。

如何解决有序性问题

指令重排序是编译期/CPU为了有可能的性能提升,而进行的擅自优化。上面也说了这种优化在有些时候会带来一些问题,这就是有序性问题,那么我们该如何解决这种问题呢?

JVM有一个原则,叫做Happens-Before原则,翻译过来就是先行发生原则。里面有8条规则:

上图截自《深入理解Java虚拟机第三版》,除了这8条以外JVM可以随便换顺序,这8条规则没必要背,一点意义都没有。

通过这个 Happens-Before 原则,就可以解决多线程的可见性和有序性问题。

下面我们来看个经典的面试题

DCL单例到底需不需要加 volatile

DCL(Double Check Lock)双重检查锁,单例模式的一种实现方案,代码如下:

java

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

    public static Singleton getSingleton() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}
  • 这段代码看起来很完美,但它是有问题的。主要在于instance = new Singleton()这句,这其实并非是一个原子操作,事实上这行代码大概做了下面 3 件事情:
  1. 分配一块内存M,成员变量赋默认值(0,0.0,false,null)
  2. 调用 Singleton 类的构造函数,在内存M上初始化成员变量
  3. 将instance变量指向分配的内存M(执行完这步 instance 就为非 null 了)

但是由于存在指令重排序的优化,上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是1-3-2,则就有可能在 3 执行完毕、2 未执行之前,被线程二抢占了CPU,去调用 getSingleton() 方法。这时 instance 已经是非 null 了(但却没有执行第二步的初始化,此时只是完成第一步的半初始化状态),所以线程二会直接返回 instance,然后使用,然后理所当然发生错误。因为此时 instance 对象还没有执行第二步 ,没有调用构造函数初始化成员变量。

这里给大家看下 new 一个对象,字节码长什么样,证实一下确实是上面说的这三个步骤,可不是我胡说:

图中红色框起来的三行字节码指令,就是上面对应的三个步骤( dup 和 return 指令在这里暂时不需要关注)。

none

0 new #2 <java/lang/Object>
4 invokespecial #1 <java/lang/Object.<init> : ()V>
7 astore_1
  • 么这个问题要怎么解决呢?其实只要将变量 instance ⽤ volatile 修饰,就可以避免这个问题了。

java

public class Singleton {

    /** 声明成 volatile */
    private volatile static Singleton instance;

    private Singleton() {}

    public static Singleton getSingleton() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}
  • 可另一个问题又来了,为什么加了 volatile ,就可以避免指令重排序导致的问题呢?

volatile 如何防止指令重排序

我们想一下,发生上面指令重排序的情况,本质上就是两行代码的顺序发生了交换。比如你站在A点,我站在B点,你我交换位置就会出现问题。那如果不想让你我交换位置,有什么办法呢?

只要给咱俩中间加一堵墙就行了嘛,你过不来,我也过不去,就不会发生位置交换了。

没错,其实 volatile 就是这么干的,这堵“墙”,就被称之为内存屏障

内存屏障,其本质上是一条特殊的屏障指令,编译器/CPU当看到这条指令的时候,就绝对不会将这条指令之前的指令,和之后的指令换顺序。

那屏障指令有哪些呢?不同的CPU,是不一样的。

我们以英特尔CPU举例,它的屏障指令有3个:lfence、mfence、sfence。这个东西是汇编级别的,暂时不用关心这些。

那Java里面有没有屏障指令呢?Java里也得有一种机制,来告诉JVM,不能随便换顺序啊。没错,这语句就是volatile

JVM在看到 volatile 之后呢,就会给被 volatile 修饰的变量加屏障指令。注意这里和缓存一致性协议没有关系,缓存一致性协议是硬件级别的东西,我们现在讲的是 Java 虚拟机中的实现。

JVM中的内存屏障一共有四种,这是JVM的规范:

  1. LoadLoad屏障
  2. StoreStore屏障
  3. LoadStore屏障
  4. StoreLoad屏障

看着有点懵,其实很简单。

以第一个LoadLoad屏障为例,有个一个变量X,它前面一条读命令,它后面也有一条读命令,中间有个LoadLoad屏障,那么前面的读命令和后面的读命令就不能换顺序。

再比如第二个StoreStore屏障,有个一个变量X,它前面有一条写命令,它后面也有一条写命令,中间有个StoreStore屏障,那么前面的写命令和后面的写命令就不能换顺序。

好了后面的两个指令就不用讲了吧。

那么就可以看 volatile 是怎么实现的了,在JVM层面:

对于被 volatile 修饰的变量,在发生写的前面,会加上StoreStore屏障,在后面会加上StoreLoad屏障。意味着前面的写完我才能写,我写完后面的才能读。

对于被 volatile 修饰的变量,在发生读的后面,会加上LoadLoad屏障,和LoadStore屏障。意味着我读完后面的才能读写。

放个图总结下:

从这个表格最后⼀列可以看出,如果第⼆个操作为 volatile 写,不管第⼀个操作是什么,都不能重排序,这就确保了 volatile 写之前的操作不会被重排序到 volatile 写之后。

从这个表格倒数第⼆⾏可以看出,如果第⼀个操作为 volatile 读,不管第⼆个操作是什么,都不能重排序,这确保了 volatile 读之后的操作不会被重排序到 volatile 读之前。

这样就从JVM上保证了变量读写的有序性。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【DDR3 控制器设计】(7)DDR3 的用户端口读写模块设计

写在前面 本系列为 DDR3 控制器设计总结&#xff0c;此系列包含 DDR3 控制器相关设计&#xff1a;认识 MIG、初始化、读写操作、FIFO 接口等。通过此系列的学习可以加深对 DDR3 读写时序的理解以及 FIFO 接口设计等&#xff0c;附上汇总博客直达链接。 【DDR3 控制器设计】系列…

CSS---复合选择器

目录 一&#xff1a;复合选择器的介绍 二、复合选择器的讲解 &#xff08;1&#xff09;后代选择器 &#xff08;2&#xff09;子元素选择器 &#xff08;3&#xff09;并集选择器 &#xff08;4&#xff09;链接伪类选择器 &#xff08;5&#xff09;focus伪类选择器 一&…

基于SpringBoot的线上买菜系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SpringBoot 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目…

【Spring系列】- Spring事务底层原理

Spring事务底层原理 &#x1f604;生命不息&#xff0c;写作不止 &#x1f525; 继续踏上学习之路&#xff0c;学之分享笔记 &#x1f44a; 总有一天我也能像各位大佬一样 &#x1f3c6; 一个有梦有戏的人 怒放吧德德 &#x1f31d;分享学习心得&#xff0c;欢迎指正&#xff0…

Vue-CLI的安装、使用及环境配置(超详细)

Vue CLI 是一个基于 Vue 进行快速项目开发的工具。它可以提供可交互式的项目脚手架和运行时的服务依赖&#xff0c;帮助你快速完成一个风格统一、拓展性强的现代化 web 单页面应用。 Vue-CLI 所需环境 Vue-CLI 是一个需要全局安装的NPM包&#xff0c;安装需要在 Node.js 环境下…

一、openCV+TensorFlow环境搭建

目录一、anaconda安装二、tensorflow安装三、Opencv安装四、pycharm新建项目使用Anaconda的环境五、验证环境安装六、tensorflow安装jupyter notebook一、anaconda安装 anaconda官网&#xff1a;https://www.anaconda.com/anaconda下载&#xff1a;https://repo.anaconda.com/…

【k8s】10.网络插件

文章目录一、etcd详解1、etcd的特点2、准备签发证书的环境二、网络插件原理1、flannel1.1 UDP模式&#xff08;性能差&#xff09;1.2 VXLAN模式&#xff08;性能较好&#xff09;1.3 host-gw模式&#xff08;性能最高&#xff09;2、calico插件3、总结一、etcd详解 etcd是Cor…

Redis_第二章_实战篇_第一节_ 短信登录

Redis_第二章_实战篇_第一节_ 短信登录 文章目录Redis_第二章_实战篇_第一节_ 短信登录短信登录1.1、导入黑马点评项目1.1.1 、导入SQL1.1.2、有关当前模型1.1.3、导入后端项目1.1.4、导入前端工程1.1.5 运行前端项目:1.2 、基于Session实现登录流程1.3 、实现发送短信验证码功…

ANDI数据集介绍|补充信息|2022数维杯国际赛C题

目录 1.患者基本信息 2.生物标记物量化值 3.认知评估 4.解剖结构量化值 5.Other 6.上述各信息的bl值 1.患者基本信息 RID (Participant roster ID) ex. 2、PTID (Original study protocol) ex. 011_S_0002、VISCODE (Visit code) ex. bl、SITE ex. 11、COLPROT (Study p…

服务拆分和远程调用(微服务)

博客主页&#xff1a;踏风彡的博客 博主介绍&#xff1a;一枚在学习的大学生&#xff0c;希望在这里和各位一起学习。 所属专栏&#xff1a;SpringCloud 文章创作不易&#xff0c;期待各位朋友的互动&#xff0c;有什么学习问题都可在评论区留言或者私信我&#xff0c;我会尽我…

课程设计 | 教学设备管理系统

&#x1f388; 作者&#xff1a;Linux猿 &#x1f388; 简介&#xff1a;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;Linux、C/C、云计算、物联网、面试、刷题、算法尽管咨询我&#xff0c;关注我&#xff0c;有问题私聊&#xff01; &…

SpringCloud微服务(二)——Eureka服务注册中心

Eureka服务注册中心 SpringCloud组件&#xff0c;Eureka已停更。 内容简介 1、Eureka是什么 Eureka 是 Netflix 开发的&#xff0c;一个基于 REST 服务的&#xff0c;服务注册与发现的组件&#xff0c;以实现中间层服务器的负载平衡和故障转移。服务注册&#xff1a;将服务…

[杂记]算法: 单调栈

0. 引言 单调栈, 顾名思义就是从栈底到栈顶元素单调递增或者单调递减的栈. 往往, 我们在解决寻找一个元素前面/后面的最远/最近处满足某条件的另一个元素的时候可以用到单调栈. 也是用两道算法题作为例子. 在这之前, 先简单写一下构造单调栈的模板. 如果我们需要从一个数组中…

ES6 入门教程 18 Iterator 和 for...of 循环 18.7 for...of 循环

ES6 入门教程 ECMAScript 6 入门 作者&#xff1a;阮一峰 本文仅用于学习记录&#xff0c;不存在任何商业用途&#xff0c;如侵删 文章目录ES6 入门教程18 Iterator 和 for...of 循环18.7 for...of 循环18.7.1 数组18.7.2 Set 和 Map 结构18.7.3 计算生成的数据结构18.7.4 类似…

供应叶酸PEG试剂Folic acid-PEG-Azide,FA-PEG-N3,叶酸-聚乙二醇-叠氮

1、名称 英文&#xff1a;Folic acid-PEG-Azide&#xff0c;FA-PEG-N3 中文&#xff1a;叶酸-聚乙二醇-叠氮 2、CAS编号&#xff1a;N/A 3、所属分类&#xff1a;Azide PEG Folic acid&#xff08;FA&#xff09; PEG 4、分子量&#xff1a;可定制&#xff0c;FA-PEG-N3 5…

Web安全之CTF测试赛

Web安全之CTF测试赛 1.000-我是谁 题目描述&#xff1a; 找到诈骗网站bsde.cn的域名注册人及邮箱&#xff0c;将域名注册人的126邮箱填写到下方答案框并点击送出 考察点&#xff1a;whois查询 whois查询网址&#xff1a; https://x.threatbook.com/ //微步在线 http://wh…

ES6 入门教程 17 Promise 对象 17.11 Promise.reject() 17.12 应用 17.13 Promise.try()

ES6 入门教程 ECMAScript 6 入门 作者&#xff1a;阮一峰 本文仅用于学习记录&#xff0c;不存在任何商业用途&#xff0c;如侵删 文章目录ES6 入门教程17 Promise 对象17.11 Promise.reject()17.12 应用17.12.1 加载图片17.12.2 Generator 函数与 Promise 的结合17.13 Promise…

ES6 入门教程 15 Proxy 15.2 Proxy 实例的方法 15.2.2 set() 15.2.3 apply()

ES6 入门教程 ECMAScript 6 入门 作者&#xff1a;阮一峰 本文仅用于学习记录&#xff0c;不存在任何商业用途&#xff0c;如侵删 文章目录ES6 入门教程15 Proxy15.2 Proxy 实例的方法15.2.2 set()15.2.3 apply()15 Proxy 15.2 Proxy 实例的方法 拦截方法的详细介绍。 15.2.…

【附源码】Python计算机毕业设计天润律师事务所管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

〖全域运营实战白宝书 - 运营角色认知篇③〗- 运营的底层逻辑是什么?

大家好&#xff0c;我是 哈士奇 &#xff0c;一位工作了十年的"技术混子"&#xff0c; 致力于为开发者赋能的UP主, 目前正在运营着 TFS_CLUB社区。 &#x1f4ac; 人生格言&#xff1a;优于别人,并不高贵,真正的高贵应该是优于过去的自己。&#x1f4ac; &#x1f4e…