JUC-指令有序性

2024-09-01 20:12
文章标签 指令 juc 有序性

本文主要是介绍JUC-指令有序性,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

指令重排

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// 首次访问会同步,而之后的使用没有 synchronizedsynchronized(Singleton.class) {if (INSTANCE == null) { // t1INSTANCE = 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) { // t1INSTANCE = 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 之间的可见性与有序性

这篇关于JUC-指令有序性的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1127929

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

工作常用指令与快捷键

Git提交代码 git fetch  git add .  git commit -m “desc”  git pull  git push Git查看当前分支 git symbolic-ref --short -q HEAD Git创建新的分支并切换 git checkout -b XXXXXXXXXXXXXX git push origin XXXXXXXXXXXXXX

Android中如何实现adb向应用发送特定指令并接收返回

1 ADB发送命令给应用 1.1 发送自定义广播给系统或应用 adb shell am broadcast 是 Android Debug Bridge (ADB) 中用于向 Android 系统发送广播的命令。通过这个命令,开发者可以发送自定义广播给系统或应用,触发应用中的广播接收器(BroadcastReceiver)。广播机制是 Android 的一种组件通信方式,应用可以监听广播来执行

mysql中导入txt文件数据的操作指令

1 表tt的格式:    CREATE TABLE `tt` (   `ind` int NOT NULL auto_increment,   `name` char(100) default NULL,   PRIMARY KEY  (`ind`)  )   2 文件d.txt的内容示例:  1,a  2,b  3,c

可选择的反思指令微调

论文:https://arxiv.org/pdf/2402.10110代码:GitHub - tianyi-lab/Reflection_Tuning: [ACL'24] Selective Reflection-Tuning: Student-Selected Data Recycling for LLM Instruction-Tuning机构:马里兰大学, Adobe Research领

驱动安装注册表指令

HKCR: HKEY_CLASSES_ROOT HKCU: HKEY_CURRENT_USER HKLM: HKEY_LOCAL_MACHINE HKU: HEKY_USER HER: 相对根键

4G模块、WIFI模块、NBIOT模块通过AT指令连接华为云物联网服务器(MQTT协议)

MQTT协议概述 MQTT(Message Queuing Telemetry Transport)是一种轻量级的消息传输协议,它被设计用来提供一对多的消息分发和应用之间的通讯,尤其适用于远程位置的设备和高延迟或低带宽的网络。MQTT协议基于客户端-服务器架构,客户端可以订阅任意数量的主题,并可以发布消息到这些主题。服务器(通常称为MQTT Broker)则负责接受来自客户端的连接请求,并转发消

matlab一些基本重要的指令

重点内容 运行MATLAB的帮助demo,在Command Window 输入 “demo”,或在Launch Pad 选项卡“demos” 任何时候都可以: 清除Command Window内容:clc 清除Figure Window(图形窗口) clf 清除workspace 变量内容: clear 注意:M脚本文件和输入指令中的变量都在workspace中,为避免变量冲突,一般在

【编程底层思考】详解Java的JUC多线程并发编程底层组件AQS的作用及原理

Java中的AbstractQueuedSynchronizer(简称AQS)是位于java.util.concurrent.locks包中的一个核心组件,用于构建锁和其他同步器。AQS为实现依赖于FIFO(先进先出)等待队列的阻塞锁和相关同步器提供了一套高效、可扩展的框架。 一、AQS的作用 统一同步状态管理:AQS提供了一个int类型的成员变量state,用于表示同步状态。子类可以根据自己

vue的v-slot指令使用总结

父组件代码:  <template><div id="app"><img alt="Vue logo" src="./assets/logo.png"><slotdemo> <template v-slot:a>this is a </template>asdad</slotdemo></div></template><script>import slotdemo from './compon