JUC-指令有序性

news2024/9/21 20:53:55

指令重排

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; 
j = ...; 

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是


i = ...; 
j = ...;

也可以是

j = ...;
i = ...; 

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU 执行指令的原理来理解一下吧

原理之指令级并行

名词

Clock Cycle Time

主频的概念大家接触的比较多,而 CPU 的 Clock Cycle Time(时钟周期时间),等于主频的倒数,意思是 CPU 能 够识别的最小时间单位,比如说 4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns,作为对比,我们墙上挂钟的 Cycle Time 是 1s

例如,运行一条加法指令一般需要一个时钟周期时间

CPI

有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction)指令平均时钟周期数

IPC

IPC(Instruction Per Clock Cycle) 即 CPI 的倒数,表示每个时钟周期能够运行的指令数

CPU 执行时间

程序的 CPU 执行时间,即我们前面提到的 user + system 时间,可以用下面的公式来表示

程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time

指令重排序优化

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令 还可以再划分成一个个更小的阶段,

例如每条指令都可以分为:

取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段

术语参考

  • instruction fetch (IF)

  • instruction decode (ID)

  • execute (EX)

  • memory access (MEM)

  • register write back (WB)

在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序组合来实现指令级并行,这一技术在 80's 中 叶到 90's 中叶占据了计算架构的重要地位。  

 指令重排的前提是,重排指令不能影响结果,例如

// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );
// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2

支持流水线的处理器

现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理 器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一 条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了 指令地吞吐率。

SuperScalar 处理器

大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单 元等,这样可以把多条指令也可以做到并行获取、译码等,CPU 可以在一个时钟周期内,执行多于一条指令,IPC > 1

诡异的结果

_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?

情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1

情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1

情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

结果还有可能是 0 😁😁😁,信不信吧!

这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2

相信很多人已经晕了 😵😵😵

int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
    if(ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}
// 线程2 执行此方法
public void actor2(I_Result r) { 
    num = 2;
    ready = true; 
}

解决方法

volatile 修饰的变量,可以禁用指令重排

原理之 volatile

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障

  • 对 volatile 变量的读指令前会加入读屏障

如何保证可见性

  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

public void actor2(I_Result r) {    
num = 2;    
ready = true; // ready 是 volatile 赋值带写屏障    // 写屏障 
}
  • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

public void actor1(I_Result r) {   
 // 读屏障    // ready 是 volatile 读取值带读屏障    
if(ready) {       
 r.r1 = num + num;  
 } 
else {       
 r.r1 = 1; 
  }
 }


如何保证有序性  

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后,因为写屏障之前的数据要同步到主存中,如果进行指令的重排,有些数据的同步就不到位,所以都不能重排

public void actor2(I_Result r) {
    num = 2;
    ready = true; // ready 是 volatile 赋值带写屏障
    // 写屏障
}
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

public void actor1(I_Result r) {
    // 读屏障
    // ready 是 volatile 读取值带读屏障
    if(ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}

还是那句话,不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其它线程的读操作跑到它前面去

  • 而有序性的保证也只是保证了本线程内相关代码不被重排序

double-checked locking 问题

public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;
    public static Singleton getInstance() { 
        if(INSTANCE == null) { // t2
            // 首次访问会同步,而之后的使用没有 synchronized
            synchronized(Singleton.class) {
                if (INSTANCE == null) { // t1
                    INSTANCE = new Singleton();
                } 
            }
        }
        return INSTANCE;
    }
}

以上的实现特点是:

  • 懒惰实例化

  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁

  • 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外

 但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:

0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn

其中

  • 17 表示创建对象,将对象引用入栈 // new Singleton

  • 20 表示复制一份对象引用 // 引用地址

  • 21 表示利用一个对象引用,调用构造方法

  • 24 表示利用一个对象引用,赋值给 static INSTANCE

也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:  

关键在于 0: getstatic 这行代码在 monitor (synchronized代码块之外)控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取 INSTANCE 变量的值

这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例

对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效

double-checked locking 解决

public final class Singleton {
    private Singleton() { }
    private static volatile Singleton INSTANCE = null;
    public static Singleton getInstance() {
        // 实例没创建,才会进入内部的 synchronized代码块
        if (INSTANCE == null) { 
            synchronized (Singleton.class) { // t2
                // 也许有其它线程已经创建实例,所以再判断一次
                if (INSTANCE == null) { // t1
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
// -------------------------------------> 加入对 INSTANCE 变量的读屏障
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter -----------------------> 保证原子性、可见性
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
// -------------------------------------> 加入对 INSTANCE 变量的写屏障
27: aload_0
28: monitorexit ------------------------> 保证原子性、可见性
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn

如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面 两点:

  • 可见性

    • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中

    • 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据

  • 有序性

    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

  • 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性

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

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

相关文章

大数据技术之HBase优化(5)

目录 HBase 优化 RowKey 设计 实现需求 1 实现需求 2 添加预分区优化 参数优化 JVM 调优 HBase 使用经验法则 HBase 优化 RowKey 设计 一条数据的唯一标识就是 rowkey&#xff0c;那么这条数据存储于哪个分区&#xff0c;取决于 rowkey 处于哪个预分区的区间内。设计 rowkey …

音乐发烧友的蓝牙耳机推荐?四款开放式耳机推荐

蓝牙耳机我还是很推荐开放式耳机的&#xff0c;因为开放式耳机相较入耳式耳机佩戴起来真的很舒适。 不入耳的设计&#xff0c;能够给耳道带来足够的“呼吸空间”。还有许多主流的开放式耳机&#xff0c;像是大部分的挂耳式的蓝牙耳机&#xff0c;耳挂部分都是使用的耐用且易弯…

【吊打面试官系列-Redis面试题】Redis 的持久化机制是什么?各自的优缺点?

大家好&#xff0c;我是锋哥。今天分享关于 【Redis 的持久化机制是什么&#xff1f;各自的优缺点&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; Redis 的持久化机制是什么&#xff1f;各自的优缺点&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Jav…

网站开发:XTML+CSS - 网页文档结构

1. 前言 HTML&#xff08;HyperText Markup Language&#xff0c;超文本标记语言&#xff09;是构建网页和 web 应用程序的标准标记语言。它定义了网页的结构和内容&#xff0c;允许开发者创建有组织、语义化的文档。 HTML 使用一系列的元素&#xff08;elements&#xff09;和…

[ABC368G] Add and Multiply Queries

G - Add and Multiply Queries 2个单点&#xff0c;一个询问&#xff0c;感觉询问只能O(n)做 但是发现查询答案保证在1e18以下 2^60 也就是说b[i]>1最多60个 也就是说需要判断的地方最多60个&#xff0c;其他地方可以用区间和优化 用set or vector 记录b[i]>1的位置&a…

如何在 Python 中将字符串转换为日期时间或时间对象

文章目录 一、介绍二、datetime使用将字符串转换为对象datetime.strptime()三、将字符串转换为datetime.datetime()对象示例四、将字符串转换为datetime.date()对象示例五、将字符串转换为datetime.time()对象示例六、datetime.datetime()使用区域设置示例将字符串转换为对象七…

最小二乘模型和线性最小均方模型的区分

禹晶、肖创柏、廖庆敏《数字图像处理&#xff08;电子信息前沿技术丛书&#xff09;》P229、P230 这部分是最小二乘模型&#xff0c;而维纳滤波是建立在随机过程总体的基础上线性最小均方模型&#xff0c;所以这样的推导过程从概念上错误&#xff0c;删除。 后悔没听廖老师的。…

更改银河麒麟服务器的语言环境为中文

更改银河麒麟服务器的语言环境为中文 1、查看语言环境2、更改语言环境 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 1、查看语言环境 打开终端&#xff0c;运行&#xff1a; locale -a查看是否包含zh_CN.UTF-8。 2、更改语言环境 编辑文…

无人机反制:低空安全综合管理平台技术详解

无人机反制技术中的低空安全综合管理平台&#xff0c;作为守护低空安全的重要工具&#xff0c;集成了多种先进的技术手段和管理功能&#xff0c;实现了对无人机等低空飞行器的全方位、无死角监控与反制。以下是对该技术平台的详细解析&#xff1a; 一、技术架构与核心功能 低…

Vue笔记总结(Xmind格式):第五天

Xmind鸟瞰图&#xff1a; 简单文字总结&#xff1a; 1. 生命周期 Vue 框架内置函数&#xff0c;随着组件的生命周期阶段&#xff0c;自动执行 作用: 特定的时间点&#xff0c;执行特定的操作 场景: 组件创建完毕后&#xff0c;可以在created 生命周期函数中发起Ajax 请求&am…

2024/8/31 笔记

IOC&DI 之前写了一点&#x1f447; 7/8 复盘-CSDN博客 事务管理 为什么需要事务管理 &#x1f446;一般加在业务层增删改的方法上&#xff0c;查询或者简单的增删改基本用不上 事务的回滚 rollbackForException.class //设置所有异常都会回滚&#x1f447; 事务…

华为云征文|下一代云服务器,Flexus X实例选购指导

华为云 Flexus 服务器所提供的 Flexus X 实例&#xff0c;是基于擎天 QingTian 架构、瑶光云脑、盘古大模型等根技术的创新&#xff0c;基于业务负载可灵活匹配业务产品所需要的资源规格&#xff0c;同时是业界内首款基于应用驱动的柔性算力云服务器。 Flexus X 实例具有大模型…

Java18 设计模式

第十八节&#xff1a;设计模式 1.设计模式概述 1.1软件设计模式的产生背景 ​ "设计模式"最初并不是出现在软件设计中&#xff0c;而是被用于建筑领域的设计中。1977年美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任克里斯托夫亚历山大&#xff08;…

谁便宜就选谁---基于成本的优化

什么是成本 我们之前老说MySQL执行一个查询可以有不同的执行方案&#xff0c;它会选择其中成本最低&#xff0c;或者说代价最低的那种方案去真正的执行查询。不过我们之前对成本的描述是非常模糊的&#xff0c;其实在MySQL中一条查询语句的执行成本是由下面这两个方面组成的&a…

十四、坦克大战(上)

文章目录 一、坦克大战游戏演示(略)二、java绘图坐标体系三、java绘图技术3.1 快速入门3.2 绘图原理Graphics类 四、java事件处理机制五、绘制坦克5.1 坦克抽象类、己方坦克、敌方坦克5.2 自定义面板5.3 绘图界面 一、坦克大战游戏演示(略) 二、java绘图坐标体系 三、java绘图…

LVGL 控件之日历(lv_calendar)

目录 一、日历1、组成2、创建日历3、日期的设置/显示3.1 设置当前日期3.2 显示日期3.3 日历头 4、设置日期高亮5、设置日名6、事件7、API 函数 一、日历 1、组成 日历部件由两个部分组成&#xff1a; 主体背景 LV_PART_MAIN&#xff1b;各个按钮 LV_PART_ITEMS&#xff08;指…

kubeadm部署 Kubernetes(k8s) 高可用集群【V1.20 】

kubeadm是官方社区推出的一个用于快速部署kubernetes集群的工具。 calico.yaml kubernertes-dashboard.yaml 1. 安装要求 在开始之前&#xff0c;部署Kubernetes集群机器需要满足以下几个条件&#xff1a; 7台机器&#xff0c;操作系统Openeuler22.03 LTS SP4硬件配置&#…

【GPT】Coze使用开放平台接口-【1】创建插件

本文档主要描述在 coze 里面把开放平台的接口创建为插件&#xff0c;供别人调用。本系列调用的接口&#xff0c;都是调用快商通 AI 开放平台的接口 注意&#xff1a;如果是团队需要的&#xff0c;建议直接在团队空间里面创建这些&#xff0c;不然在个人空间创建得很爽&#xff…

Linux——命令行文件的管理(创建,复制,删除,移动文件,硬链接与软链接)

目录 一、创建文件和目录 二、复制文件和目录 三、删除文件和目录 四、移动文件和目录 五、硬链接和软链接&#xff08;软链接也指符号链接&#xff09; 索引节点&#xff08;inode) 硬链接 软链接&#xff08;符号链接&#xff09; 一、创建文件和目录 mkdir命令可以创…

PCIe 复位:必须了解的PERST#

1.什么是PERST# PERST#作为 Fundamental Reset&#xff0c;是直接通过边带信号PERST#&#xff08;PCI Express Reset&#xff09;产生的。Fundamental Reset会复位整个PCIe设备&#xff0c;初始化所有与状态机相关的硬件逻辑&#xff0c;端口状态以及配置空间中的配置寄存器…