JVM指令重排序

news2024/11/26 20:02:35

文章目录

  • 什么是指令重排序
    • 编译器优化
    • 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硬件提供了一系列的内存屏障,主要有:

  1. lfence:load fence,读屏障指令。在lfence指令前的读操作必须在lfence指令后的读操作前完成。即读串行化。
  2. sfence:save fence,写屏障指令。在sfence指令前的写操作必须在sfence指令后的写操作前完成。即写串行化。
  3. mfence:modify/mix fence,混合屏障指令,是一种全能型的屏障。在mfence指令前的读写操作必须在mfence指令后的读写操作前完成。即读写串行化。
  4. 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中提供了四类内存屏障指令:

屏障类型指令示例说明
LoadLoadLoad1; LoadLoad; Load2保证load1的读取操作在load2及后续读取操作之前执行
StoreStoreStore1; StoreStore; Store2在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStoreLoad1; LoadStore; Store2在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoadStore1; 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 内存模型(二)——重排序

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

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

相关文章

AICon 全球人工智能与机器学习技术大会参会有感

目录 引言 大会背景 大会议程 参会体验 会后感想 结束语 引言 在数字化浪潮席卷全球的今天,人工智能和机器学习技术已成为推动社会进步和产业升级的关键力量。作为一名对AI技术非常感兴趣的开发者,在 8 月 18 日至 19 日这两天,我有幸参…

代码随想录算法训练营43期 | Day 15——110.平衡二叉树、 257. 二叉树的所有路径、404. 左叶子之和、222. 完全二叉树的节点个数

代码随想录算法训练营 代码随想录算法训练营43期 | Day 15110.平衡二叉树257. 二叉树的所有路径404. 左叶子之和222. 完全二叉树的节点个数 代码随想录算法训练营43期 | Day 15 110.平衡二叉树 /*** Definition for a binary tree node.* struct TreeNode {* int val;* …

【读点论文】Adaptive degraded document image binarization,真难复现,流程有点复杂

Adaptive degraded document image binarization Abstract 本文介绍了一种新的自适应方法,用于对退化文档进行二值化和增强。所提出的方法不需要用户进行任何参数调整,并且可以处理由于阴影、不均匀照明、低对比度、大信号相关噪声、污点和应变而发生的…

Spring 的 Aop 支持

Spring 的 Aop 支持 一、AOP基本概念1_AOP基本术语2_通知类型回顾3_AOP基于代理4_SpringBoot整合AOP 二、整体流程剖析1_AspectJAutoProxyRegistrar2_AnnotationAwareAspectJAutoProxyCreator1_继承结构2_初始化时机3_AnnotationAwareAspectJAutoProxyCreator的作用时机 3_收集…

【C++】_string类字符串详细解析(1)

假如没有给你生命,你连失败的机会都没有。你已经得到了最珍贵的,还需要抱怨什么!💓💓💓 目录 ✨说在前面 🍋知识点一:什么是string? •🌰1.string类的概念 •&#x1…

嘉立创PCB4层板

视频: 四层板PCB设计保姆级教程(1):3.0HUB设计概述_哔哩哔哩_bilibili(虽然是四层板实际这个还是两层板!) 不太建议看这个。 四层PCB 最简单终教学 高校培训课程 深入浅出 不会电路也能学会 设…

Nginx的核心!!! 负载均衡、反向代理

目录 负载均衡 1.轮询 2.最少连接数 3.IP哈希 4.加权轮询 5.最少时间 6.一致性哈希 反向代理 测试 之前讲过Nginx 的简介和正则表达式,那些都是Nginx较为基础的操作,Nginx 最重要的最核心的功能,当属反向代理和负载均衡了。 负载均…

YOLOv8实例分割+双目相机实现物体尺寸测量

1,YOLOv8实例分割原理介绍 YOLOv8是YOLO系列的最新版本,它在目标检测和实例分割方面都进行了显著的改进和创新。以下是YOLOv8实例分割原理的一些关键点: 先进的骨干和颈部架构:YOLOv8采用了先进的骨干和颈部架构来提高特征提取和物…

分治,1875C - Jellyfish and Green Apple

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 1875C - Jellyfish and Green Apple 二、解题报告 1、思路分析 n 个苹果…

小阿轩yx-Kubernetes Pod入门

小阿轩yx-Kubernetes Pod入门 前言 Kubernetes 中 一个重要的概念就是 Pod(豆荚)并不直接管理容器,它的最小管理单元叫做 Pod。 Docker 应用中 把一个应用程序封装在一个镜像中,之后启动这个镜像并映射个宿主机端口号&#x…

03、Redis实战:商户查询缓存、缓存更新策略、缓存穿透、缓存雪崩、缓存击穿

2、商户查询缓存 2.1 什么是缓存? 什么是缓存? 就像自行车,越野车的避震器 举个例子:越野车,山地自行车,都拥有"避震器",防止车体加速后因惯性,在酷似"U"字母的地形上飞跃,硬着陆导致的损害,像个弹簧一样; 同样,实际开发中,系统也需要"避震器&qu…

2、Unity【基础】Mono中的重要内容

Unity基础 MonoBehavior中的重要内容 文章目录 Mono中的重要内容1、延迟函数1、延迟函数概念2、延迟函数使用3、延迟函数受对象失活销毁影响思考1 利用延时函数实现计时器思考2 延时销毁 2、协同程序1、Unity是否支持多线程2、协同程序概念3、协同程序和线程的区别4、协程的使用…

APP架构设计_1.官方应用架构指南

1.官方应用架构指南 1.1架构的原则 应用架构定义了应用的各个部分之间的界限以及每个部分应承担的职责。谷歌建议按照以下原则设计应用架构。 分离关注点通过数据模型驱动界面单一数据源单向数据流 1.2谷歌推荐的应用架构 每个应用应至少有两个层: 界面层 - 在屏…

近视防控明星:蔡司小乐圆中期临床数据详解

近视防控明星:蔡司小乐圆中期临床数据详解 小乐圆镜片作为近视防控镜片里的明星产品,从22年5月上市以来防控效果就一直备受大家的关注。而最近中期临床试验的结果,给家长孩子吃了一颗定心丸。 本次实验中,240位受试者被随机分成三…

基于springboot框架的电影订票系统_wqc3k

TOC springboot611基于springboot框架的电影订票系统_wqc3k--论文 绪 论 1.1研究背景和意义 随着科学技术的不断发展,计算机现在已经成为了社会的必需品,人们通过网络可以获得海量的信息,这些信息可以和各行各业进行关联,电影…

你应该停止使用的 7 个已弃用的 Python 库

欢迎来到雲闪世界。升级您的 Python 工具包:发现 7 个应停止使用的过时库以及替代它们的功能。最近,我回顾了 Python 的新特性,发现每个版本都引入了创新,使我们的日常开发工作变得更加轻松。 这让我意识到科技是一门永无止境的艺…

8.21 QT

1.思维导图 2. 服务器端 头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTcpServer>//服务器类 #include <QMessageBox> #include <QDebug> #include <QList> #include <QTcpSocket>QT_BEGIN_NAMESPACE names…

免费高画质提取PPT/Word/Excel中的图片工具

下载地址&#xff1a;https://pan.quark.cn/s/134ccc35b8a2 软件简介&#xff1a; 好不容易搞到一个几十上百MB的ppt&#xff0c;想导出里面的图片进行二次加工&#xff0c;却被ppt超低画质的图片另存为功能劝退&#xff0c;明知里面全是高清图片&#xff0c;走时却是两手空空…

【C++从练气到飞升】14---深入浅出继承

&#x1f388;个人主页&#xff1a;库库的里昂 ✨收录专栏&#xff1a;C从练气到飞升 &#x1f389;鸟欲高飞先振翅&#xff0c;人求上进先读书&#x1f389; 目录 ⛳️推荐 一、继承的概念及定义 1.1 继承的概念 1.2 继承定义 1.2.1 定义格式 1.2.2 继承方式和访问限定符…

重新认识AbstractQueuedSynchronizer

开篇之前&#xff0c;烦请诸位允许我附庸风雅一次。近期因诸事繁杂&#xff0c;心情颇低落&#xff0c;遂于喜马拉雅APP中收听《老子》一文。其中的第八十一章《结天道》一文于我感悟颇深&#xff1a;和大怨&#xff0c;必有余怨&#xff0c;报怨以德&#xff0c;焉可以为善&am…