JVM指令重排序
文章目录
- 什么是指令重排序
- 编译器优化
- JIT 编译优化
- 处理器优化
- 重排序数据依赖性
- 硬件层的内存屏障
- 指令重排的代码验证
- 好处
- 减少管道阻塞
- 提高缓存利用率
- 利用并行执行单元
- 性能提升
- 更好地利用硬件资源
- 问题
- 内存可见性问题
- 编程复杂性增加
- 调试困难
- 解决方案:Java内存模型(JMM)和关键字
- Java内存模型(JMM):
- 关键字 volatile:
- 关键字 synchronized:
- 总结
- 参考
在 Java 中,指令重排是一种性能优化技术,它涉及到编译器和处理器对程序中指令的执行顺序进行调整,以提高执行效率。
什么是指令重排序
指令重排序是一种在编译器和处理器级别发生的优化过程,它改变了程序原有的指令执行顺序。这种优化可以在多个层面上发生,包括编译器优化、即时编译优化(JIT),以及处理器层面的优化。
编译器优化
当 Java 代码被编译成字节码时,Java 编译器可能会重新排列指令的顺序。这种重排序基于以下原则:
- 独立性:如果两个指令之间没有直接的数据依赖关系,编译器可能会改变它们的顺序。
- 性能提升:重排序旨在优化程序的执行,例如通过减少指令之间的延迟或改善分支预测。
- 内存访问优化:编译器可能会重新排列内存访问指令以减少缓存未命中的情况。
JIT 编译优化
Java 运行时的即时编译器(JIT)进一步优化已经编译的字节码。JIT 在程序执行时进行优化,因此它能够根据当前的执行上下文和运行时信息进行更精细的优化。例如:
- 基于热点代码的优化:JIT 会识别程序中的热点(频繁执行的代码区域)并对这些区域进行专门优化。
- 动态分析:JIT 能够根据程序的实时性能数据调整优化策略。
处理器优化
现代处理器在执行指令时,也会进行自己的重排序。这是为了更有效地利用处理器资源,如执行单元、寄存器和缓存。处理器级的指令重排序基于以下原则:
- 并行执行:处理器会尝试并行执行多个独立的指令,以提高执行效率。
- 流水线优化:处理器使用流水线技术来执行指令。通过重排序,处理器可以减少流水线阻塞和等待时间。
- 数据依赖性和冒险:处理器会分析指令之间的数据依赖性,确保重排序不会影响程序的正确执行。
重排序数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:
名称 | 示例 | 说明 |
---|---|---|
写后读 | a=1;b=a; | 写一个变量后,再读这个变量 |
写后写 | a=1;a=2; | 写一个变量后,再写这个变量 |
读后写 | a=b;b=1; | 读一个变量后,再写这个变量 |
上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。
硬件层的内存屏障
Intel硬件提供了一系列的内存屏障,主要有:
- lfence:load fence,读屏障指令。在lfence指令前的读操作必须在lfence指令后的读操作前完成。即读串行化。
- sfence:save fence,写屏障指令。在sfence指令前的写操作必须在sfence指令后的写操作前完成。即写串行化。
- mfence:modify/mix fence,混合屏障指令,是一种全能型的屏障。在mfence指令前的读写操作必须在mfence指令后的读写操作前完成。即读写串行化。
- Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对 CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由 JVM来为不同的平台生成相应的机器码。 JVM中提供了四类内存屏障指令:
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad | Load1; LoadLoad; Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
StoreStore | Store1; StoreStore; Store2 | 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存 |
LoadStore | Load1; LoadStore; Store2 | 在stroe2及其后的写操作执行前,保证load1的读操作已读取结束 |
StoreLoad | Store1; StoreLoad; Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行 |
内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个:
一是保证特定操作的执行顺序
二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条Memory Barrier则会告诉 编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插 入内存屏障禁止在内存屏障前后的指令执行重排序优化。
Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。 下面看一个非常典型的禁止重排优化的例子DCL,如下:来看一个单例模式
public class Singleton {private static Singleton instance;private Singleton(){}private static Singleton getInstance() {// 第一次检查if (instance == null) {synchronized (Singleton.class) {if (instance == null) {//多线程环境下可能出问题instance = new Singleton();}}}return instance;}
}
这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。
原因在于instance = new Singleton();
这个操作不是原子性的, 它由多个操作构成,如下图:
查看字节码文件,发现一个new操作,在内存中经过了4步:
1. new 创建对象:申请内存空间,创建一个新的Singleton实例。
2. dup 复制引用:复制栈顶刚刚创建的Singleton实例引用,并将其圧入栈顶。
3. invokespecial 调用构造函数:调用Singleton的无参构造函数来初始化对象。
4. putstatic 赋值给静态字段:将栈顶刚刚初始化好的Singleton实例引用赋值给静态字段intance。
由于步骤3和步骤4可能会重排序,如下:
1. new
2. dup
3. putstatic //设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
4. invokespecial
由于步骤3和步骤4不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance 不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
那么该如何解决呢,使用volatile禁止instance变量被执行指令重排优化即可。
private volatile static Singleton instance;
指令重排的代码验证
package org.hbin.jmm;/*** @author* @Date* @Description 指令乱排证明实力 出现0,0则说明有乱排现象。*/
public class Disorder {private static int x = 0, y = 0, a = 0, b =0;public static void main(String[] args) throws InterruptedException {int i = 0;while(true) {i++;x = 0; y = 0; a = 0; b = 0;Thread t1 = new Thread(() -> {//由于线程t1先启动,可以根据自己电脑性能调整等待时间,让它等一等线程t2.//shortWait(100000);a = 1;x = b;});Thread t2 = new Thread(() -> {b = 1;y = a;});t1.start();t2.start();t1.join();t2.join();if(x == 0 && y == 0) {System.out.printf("第%d次(%d, %d)\n", i, x, y);break;}}}
}
本地运行两次的结果如下:
好处
在程序执行过程中,并非所有指令都需要按照代码中的严格顺序来执行。有些指令之间是相互独立的,这就意味着它们可以在不影响程序最终结果的情况下,改变执行顺序。这种重排序可以更有效地利用处理器资源,具体体现在以下几个方面:
减少管道阻塞
现代处理器普遍采用流水线技术来提高指令执行效率。流水线技术将指令执行分解为多个步骤,每个步骤由不同的处理器部件完成。这样,多个指令可以同时处于不同的执行阶段,从而并行处理。然而,流水线可能会因为某些指令等待必要资源(如数据或执行单元)而暂停,这称为管道阻塞。
通过指令重排序,处理器可以调整指令的执行顺序,使得正在等待某些资源的指令不会阻碍其他指令的执行。这样做可以减少流水线的停顿时间,从而提高处理器的整体效率。
提高缓存利用率
缓存是一种快速的内存,用于存储处理器频繁访问的数据。如果处理器需要的数据不在缓存中,就会产生缓存未命中,需要从较慢的主内存中获取数据,这会导致延迟。
通过重排序数据存取指令,处理器可以优化数据的缓存利用率。例如,它可能会提前执行某些数据读取指令,确保当数据真正需要时它们已经在缓存中。同样,它也可以推迟写入操作,以减少对缓存的频繁更新。
利用并行执行单元
多核处理器可以同时执行多个指令。即使在单核处理器上,也经常有多个执行单元(如算术逻辑单元、浮点单元等)可以同时工作。
指令重排序使得处理器能够更好地利用这些并行执行单元。通过重排,处理器可以同时执行原本在程序中不相邻的指令,只要这些指令之间没有直接的依赖关系。这种并行性大大提高了执行效率,特别是在执行大量独立计算的应用程序时。
指令重排序带来的好处主要集中在性能提升和更有效地利用硬件资源两个方面。下面详细解释这些好处:
性能提升
- 减少执行时间:通过重排序指令,处理器可以减少等待时间,例如等待数据从内存中加载。这是因为可以先执行与当前等待操作无关的其他指令。
- 提高流水线效率:现代处理器通过流水线技术并行处理多个指令。重排序可以减少流水线中的空闲周期,因此更多的指令可以同时处于不同的执行阶段,从而提高整体的处理速度。
- 并行处理加速:在多核处理器中,指令重排序可以使得不同的核心同时执行不相关的任务,从而在多任务处理和并行计算中取得更高的性能。
更好地利用硬件资源
- 优化缓存使用:重排序可以优化内存访问模式,提前加载数据到缓存或推迟写操作,从而减少缓存未命中的情况。这样做可以减少从主内存获取数据的次数,提高数据访问速度。
- 利用多核优势:在多核处理器上,指令重排序可以分散计算负载,使得多个核心可以更有效地协同工作。例如,可以将计算密集型和I/O密集型任务分配给不同的核心,以提高整体效率。
- 适应现代处理器架构:现代处理器如超标量处理器,能够在每个时钟周期内发起多个指令。指令重排序使得这些处理器可以更充分地利用其并行执行能力。
问题
指令重排序虽然在提高程序性能和资源利用率方面带来了显著的好处,但它也引入了一些问题,特别是在多线程环境下。以下是这些问题的详细解释以及Java为解决这些问题提供的解决方案:
内存可见性问题
问题描述:在多线程环境下,由于每个线程可能在不同的处理器上运行,每个处理器都有自己的缓存。指令重排序可能导致一个线程对共享变量的修改对其他线程不可见。
影响:这会导致线程之间看到的共享数据状态不一致,从而产生难以预测和调试的错误。
编程复杂性增加
问题描述:为了正确地管理多线程之间的内存可见性和指令顺序,程序员需要对并发编程中的内存模型有深入的理解。
影响:这增加了编程的复杂性,特别是在处理共享数据和同步问题时。
调试困难
问题描述:由于指令重排序,程序的实际执行顺序可能与源代码中的顺序不一致。
影响:这使得调试多线程程序变得更加困难,因为观察到的行为可能与预期不符。
解决方案:Java内存模型(JMM)和关键字
Java内存模型(JMM):
JMM定义了线程和主内存之间的交互规则,确保了在多线程环境中对共享变量的访问和更新的一致性。
JMM解决了重排序可能导致的内存可见性问题,确保了在某个线程写入的值对其他线程可见。
关键字 volatile:
volatile是Java虚拟机提供的轻量级的同步机制。
-
volatile关键字有两个作用
- 保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
- 禁止指令重排序优化。
-
volatile的可见性
关于volatile的可见性作用,我们必须意识到被volatile修饰的变量对所有线程总是立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中 -
volatile无法保证原子性
-
volatile禁止指令重排
volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
关键字 synchronized:
synchronized关键字用于在某个对象上加锁,保证了多个线程在同一时刻只能有一个线程执行该代码块。
这不仅解决了多线程之间的同步问题,而且确保了锁内的操作对其他线程是可见的,因为在锁释放时会将对共享变量的修改刷新到主内存。
总结
指令重排序是一种复杂但非常有效的优化技术。它使得处理器能够更加智能地利用自身的各种资源,如流水线、缓存和并行执行单元,从而提高整体性能。然而,这种优化也带来了额外的挑战,尤其是在多线程编程中,开发者需要对这种机制有所了解,以确保程序的正确性和效率。
参考
- Java中的指令重排详解
- 深入理解 Java 内存模型(二)——重排序
相关文章:

JVM指令重排序
文章目录 什么是指令重排序编译器优化JIT 编译优化处理器优化重排序数据依赖性 硬件层的内存屏障指令重排的代码验证好处减少管道阻塞提高缓存利用率利用并行执行单元性能提升更好地利用硬件资源 问题内存可见性问题编程复杂性增加调试困难 解决方案:Java内存模型&a…...

改造字典关键字:
怎样把第一个关键字的值都 加到所有关键字上? {type: 7, typenum: , typemon: } 我们可以使用字典的keys()方法来获取所有的关键字,然后通过遍历字典的方式将第一个关键字的值添加到其他关键字的名称上。以下是一个示例代码: data {type: …...

Neo4j 图数据库入门
图形数据库存储节点和关系,而不是表或文档。数据的存储方式就像你在白板上勾画想法一样。您的数据存储不受预定义模型的限制,允许以非常灵活的方式考虑和使用它。 一、核心概念:属性图形模型 Neo4j使用属性图数据库模型。图数据结构由节点(离…...

linux 磁盘满了,程序运行失败,如何处理?df -h
场景:紧急呼救,上传图片失败了。我一脸懵,服务器这是又咋地了,别邪乎姐姐,姐姐胆子小啊。 一、寻找问题原因 1、OSS出问题了? 然后我尝试了 IOS 的APP是没问题的,Android提示上传失败…...

Python编码系列—前端后浪:Python前后端分离开发实战指南
🌟🌟 欢迎来到我的技术小筑,一个专为技术探索者打造的交流空间。在这里,我们不仅分享代码的智慧,还探讨技术的深度与广度。无论您是资深开发者还是技术新手,这里都有一片属于您的天空。让我们在知识的海洋中…...

Docker学习之路【五】了解数据卷
定义与特性 Docker数据卷是一个特殊目录,,用于实现容器间数据的持久化和共享。数据卷存在于宿主机上,可以被一个或多个容器使用。它独立于容器的生命周期,意味着即使容器被删除,数据卷中的数据也会保留。数据卷的主要…...

matlab如何设置产生的随机数一致
在MATLAB中,确保产生的随机数序列一致,通常需要使用随机数生成器的种子(seed)。通过设置相同的种子值,可以确保在每次运行代码时,随机数生成器从相同的初始状态开始,从而生成相同的随机数序列。…...

ansible --------拓展
编辑 hosts 配置文件 [rootmo ~]# vim /etc/ansible/hosts # 创建目录 [rootmo ~]# mkdir /etc/ansible/playbook # 编辑配置文件 [rootmo ~]# vim /etc/ansible/playbook/nginx.yml # 执行测试 [rootmo ~]# ansible-playbook /etc/ansible/playbook/nginx.yml roles 修…...

gazebo下使用Fast-planner配置(包含mpc局部规划+控制Gazebo小车以及FastPlanner配置)
源码链接: https://github.com/USE-jx/NMPC_CASADI_CPP?tabreadme-ov-file #这是NMPC的 里面有Fast-Planner,但编译可能缺少东西,所以再放一个Fast-Planner的,可以装装缺少的库 https://github.com/HKUST-Aerial-Robotics/Fast-P…...

Python核心编程--Python要点总结
Python 核心编程包括了一些关键的要点,理解这些要点对于掌握 Python 至关重要。以下是 Python 核心编程的一些要点: 1. 数据类型与数据结构 基本数据类型: int, float, str, bool容器类型: list, tuple, set, dict不可变类型与可变类型: tuple 是不可变…...

【mysql】mysql配置文件之优先级学习
本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》:python零基础入门学习 《python运维脚本》: python运维脚本实践 《shell》:shell学习 《terraform》持续更新中:terraform_Aws学习零基础入门到最佳实战 《k8…...

自然语言处理(NLP)大模型
自然语言处理(NLP)大模型 自然语言处理(NLP)领域中的一种重要技术,具有强大的语言理解和生成能力。以下是对NLP大模型的详细介绍: 一、定义与背景 NLP大模型是指通过大规模预训练和自监督学习技术构建的…...

融合创新趋势:Web3时代的跨界融合
随着互联网技术的飞速发展,Web3时代的到来正引领着一场深刻的技术与社会变革。Web3,作为下一代互联网技术的代表,不仅仅是一种技术创新,更是一种跨界融合的趋势。通过去中心化、智能合约和区块链技术的应用,Web3正在重…...

面临新时代的机遇与挑战,联想凌拓将如何破局?
近年来,IT行业的技术进步日新月异,云计算、大数据、人工智能……各种新兴技术犹如雨后春笋般层出不穷,并且正在给千行百业带来全面的变革甚至重塑。 然而以上提到的所有新兴技术,都离不开数据的存储与管理。那么作为中国乃至全球领…...

2024.8.21
作业: 运行1个服务器和2个客户端 实现效果: 服务器和2个客户端互相聊天,服务器和客户端都需要使用select模型去实现 服务器要监视2个客户端是否连接,2个客户端是否发来消息以及服务器自己的标准输入流 客户端要监视服务器是否发来…...

在Ubuntu16.04里安装ROS Kinetic
1.设置apt的source list sudo sh -c echo "deb http://packages.ros.org/ros/ubuntu$(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list 2.设置gpd keys sudo apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 421C365…...

后端开发刷题 | 合并两个排序的链表
描述 输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。 数据范围: 0≤n≤1000,−1000≤节点值≤1000 如输入{1,3,5},{2,4,6}时,合并后的链表为{1,2,3,4,5,6},…...

JAVA_7
JAVA_7 JAVA面向对象编程1. 抽象方法和抽象类 JAVA面向对象编程 1. 抽象方法和抽象类 使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。包含抽象方法的类就是抽象类。通过…...

最大连续1的个数 III(LeetCode)
题目 给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回 数组中连续 1 的最大个数 。 解题 def longestOnes(nums, k):left 0max_len 0zero_count 0for right in range(len(nums)):# 如果遇到0,统计当前窗口内0的个…...

Vue之前端批量下载文件并以压缩包形式存储
后端返回一个文件链接的数组,前端处理下载逻辑,并且将这些文件存储在压缩包内部,这用的jszip 和 file-saver 这两个库。 步骤说明 1.使用 npm 或 yarn 安装 jszip 和 file-saver。 npm install jszip file-saver 2.获取文件内容:…...

【AI学习】LLaMA模型的微调成本有几何?
在前面文章《LLaMA 系列模型的进化(二)》中提到了Stanford Alpaca模型。 Stanford Alpaca 基于LLaMA (7B) 进行微调,通过使用 Self-Instruct 方法借助大语言模型进行自动化的指令生成,Stanford Alpaca 生成了 52K 条指令遵循样例数…...

【专题】2024全数驱动 致胜未来-数字化敏捷银行白皮书报告合集PDF分享(附原数据表)
原文链接: https://tecdat.cn/?p37404 政策明确发展使命,新时代商业银行应坚持党建引领,秉持高质量发展理念。数字经济已成大势,商业银行需构建数字基础设施能力,强化顶层战略规划。当前商业银行数字化发展面临诸多挑…...

280Hz显示器哪家强
280Hz显示器哪家强?今天就给大家带来6大品牌和型号的280Hz显示器一起对比对比! 1.280Hz显示器 - HKC G27H3显示器 HKC G27H3是一款高性价比的电竞显示器,以下是它的一些特点: - **高刷新率与快速响应**: - 拥有280H…...

ROUTE_STATUS
ROUTE_STATUS是一个只读属性,由Vivado路由器分配给网络 反映网络上路由的当前状态。 该属性可以由单个网络或一组网络使用 get_property或report_property命令。该物业由 report_route_status命令返回整个设计的route_status。 架构支持 所有架构。 适用对象 •网络…...

v4l2(video4linux2) yuyv(yuv422)、MJPEG、H.264
V4L2(Video4Linux2)是Linux内核中的视频设备接口框架,专门用于捕获和输出视频数据。V4L2广泛应用于各种视频设备的驱动程序开发,如网络摄像头、电视调谐器、视频采集卡、以及其他视频输入/输出设备。 ### V4L2的主要功能 1. **视…...

.Net插件开发开源框架
在.NET开发中,有许多开源框架可以用于插件开发,以下是一些最常见的框架: MEF(Managed Extensibility Framework) MEF是一个用于创建可插拔软件应用程序的库,它可以在不修改原始应用程序的情况下扩展应用程…...

基于Spark实现大数据量的Node2Vec
基于Spark实现大数据量的Node2Vec Node2Vec 是一种基于图的学习算法,用于生成图中节点的低维度、高质量的向量表示。这种算法基于 word2vec 模型,将自然语言处理中的词嵌入技术应用于图结构的节点,以捕捉节点之间的复杂关系。Node2Vec 特别强…...

[VMware]VMware-Esxi 6.7 厚置备转为精简置备
背景:创建了一个win10 60G的厚置备磁盘,现在想改为精简置备。 先关闭win10系统,并删除快照 1、开启shell 2、登录到虚拟存放的目录 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [rootxxx:~] cd /vmfs/volumes/5fea055e-458157d3-c8f8-8cec4ba51c4…...

vue面试题十八
一、Vue 3中的样式绑定有哪些新特性? Vue 3中的样式绑定保持了与Vue 2相似的灵活性和强大功能,同时引入了一些新的特性和改进,主要集中在响应式系统和Composition API上。以下是Vue 3中样式绑定的主要新特性及其说明: 1. 响应式…...

windows C++-windows C++/CX简介(三)
^类型 (^) 是 C/CX 最突出的功能之一——当人们第一次看到 C/CX 代码时,很难不注意到它。那么,^ 类型到底是什么?这是类型是一种智能指针类型,它自动管理 Windows 运行时对象的生命周期,也 提供自动类型转换功能以简化…...