什么是有序性?

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

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

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

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

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

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

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

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

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 四个变量,一共有四行代码:

a = 1;
x = b;
b = 1;
y = a;

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

现在我们来简单推理下,假设程序严格按照代码的顺序去执行,那么两个线程修改完成后,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;,第二个线程里也是如此。由此可以证明,指令重排序的存在。

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

DCL单例到底需不需要加 volatile

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

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 指令在这里暂时不需要关注)。

0 new #2 <java/lang/Object>
4 invokespecial #1 <java/lang/Object.<init> : ()V>
7 astore_1

那么这个问题要怎么解决呢?其实只要将变量 instance ⽤ volatile 修饰,就可以避免这个问题了。

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

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

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

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

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

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

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

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

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

对于被volatile修饰的变量,在发生写的前面,会加上StoreStore屏障,在后面会加上StoreLoad屏障。

对于被volatile修饰的变量,在发生读的后面,会加上LoadLoad屏障,和LoadStore屏障。

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

好了,今天就到这里,下次有空再聊聊最难的原子性。

并发bug之源(二)-有序性的更多相关文章

  1. 并发Bug之源有三,请睁大眼睛看清它们

    写在前面 生活中你一定听说过--能者多劳 作为 Java 程序员,你一定听过--这个功能请求慢,能加一层缓存或优化一下 SQL 吗? 看过中国古代神话故事的也一定听过--天上一天,地上一年 一切设计来 ...

  2. 并发bug之源(一)-可见性

    CPU三级缓存 要聊可见性,这事儿还得从计算机的组成开始说起,我们都知道,计算机由CPU.内存.磁盘.显卡.外设等几部分组成,对于我们程序员而言,写代码主要关注CPU和内存两部分.放几张马士兵老师的图 ...

  3. Java并发编程之CAS二源码追根溯源

    Java并发编程之CAS二源码追根溯源 在上一篇文章中,我们知道了什么是CAS以及CAS的执行流程,在本篇文章中,我们将跟着源码一步一步的查看CAS最底层实现原理. 本篇是<凯哥(凯哥Java: ...

  4. 并发工具CyclicBarrier源码分析及应用

      本文首发于微信公众号[猿灯塔],转载引用请说明出处 今天呢!灯塔君跟大家讲: 并发工具CyclicBarrier源码分析及应用 一.CyclicBarrier简介 1.简介 CyclicBarri ...

  5. java 并发编程——Thread 源码重新学习

    Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...

  6. 【Java并发编程】之二:线程中断

    [Java并发编程]之二:线程中断 使用interrupt()中断线程 ​ 当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一 ...

  7. java并发编程笔记(二)——并发工具

    java并发编程笔记(二)--并发工具 工具: Postman:http请求模拟工具 Apache Bench(AB):Apache附带的工具,测试网站性能 JMeter:Apache组织开发的压力测 ...

  8. 并发编程Bug起源:可见性、有序性和原子性问题

    以前古老的DOS操作系统,是单进行的系统.系统每次只能做一件事情,完成了一个任务才能继续下一个任务.每次只能做一件事情,比如在听歌的时候不能打开网页.所有的任务操作都按照串行的方式依次执行. 这类服务 ...

  9. 并发编程(十二)—— Java 线程池 实现原理与源码深度解析 之 submit 方法 (二)

    在上一篇<并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)>中提到了线程池ThreadPoolExecutor的原理以及它的execute方法.这篇文章是接着上一篇文章 ...

  10. JDK中的并发bug?

    最近研究Java并发,无意中在JDK8的System.console()方法的源码中翻到了下面的一段代码: private static volatile Console cons = null; / ...

随机推荐

  1. Linux虚拟机报错grub rescue解决步骤

    /boot 分区内核文件丢失 实验准备 1) 准备:rm -rf /boot/* 2) 系统启动报错截图 修复步骤 重启显示logo时 按 Esc,选择从光驱启动 或者关机再选择打开电源时进入固件 移 ...

  2. Jenkins JNLP方式启动 Agent

    Jenkins Server配置 如果你是通过Nginx代理了jenkins,那么需要调整下Nginx的配置 map $http_upgrade $connection_upgrade { defau ...

  3. OID 与隐含列

    熟悉PostgreSQL的都知道,PG12 开始,不再支持OID伪列.KingbaseES 为了保证与旧版本兼容,特增加了OID 隐含列的支持. R3版本:OID 是整个数据库共用的"序列& ...

  4. KingbaseES图形化安装未弹出界面应该如何处理

      关键字: KingbaseES.X Windows.Gnome.DISPLAY 一.Linux下图形安装环境要求 1) 系统首先安装了X Windows的图形化支持软件包. 2) 系统安装了KDE ...

  5. git hooks在业务中的使用

    起因 最近公司项目发生了一起线上事故,最后排查下来是配置文件的问题.项目里application.yml文件内会用@build.time@记录打包时的时间,但是这个写法是build-helper-ma ...

  6. kubeadm init 命令执行流程

  7. Maven快速配置和入门

    概念 Maven其实就是一个管理项目.构建项目的工具.它有标准化的项目结构.构建流程.依赖管理. 功能 Maven提供了一套标准的项目结构 Maven提供了一套标准的构建流程 Maven提供了更便捷的 ...

  8. Visual Studio 2022 开发 STM32 单片机 - 环境搭建点亮LED灯

    安装VS2022社区版软件 选择基础的功能就好 安装VisualGDB软件(CSDN资源) 按照提示一步一步安装就好 VisualGDB激活软件(CSDN资源) 将如下软件放在VisualGDB的安装 ...

  9. MiniWord .NET Word模板引擎,藉由Word模板和数据简单、快速生成文件。

    Github / Gitee QQ群(1群) : 813100564 / QQ群(2群) : 579033769 介绍 MiniWord .NET Word模板引擎,藉由Word模板和数据简单.快速生 ...

  10. PAT (Basic Level) Practice 1032 挖掘机技术哪家强 分数 20

    为了用事实说明挖掘机技术到底哪家强,PAT 组织了一场挖掘机技能大赛.现请你根据比赛结果统计出技术最强的那个学校. 输入格式: 输入在第 1 行给出不超过 105 的正整数 N,即参赛人数.随后 N  ...