食品公司网站设计项目,直接做网站的软件,58同城怎么做网站,济南建站公司哪有文章目录 什么是指令重排序编译器优化JIT 编译优化处理器优化重排序数据依赖性 硬件层的内存屏障指令重排的代码验证好处减少管道阻塞提高缓存利用率利用并行执行单元性能提升更好地利用硬件资源 问题内存可见性问题编程复杂性增加调试困难 解决方案#xff1a;Java内存模型Java内存模型JMM和关键字Java内存模型JMM关键字 volatile关键字 synchronized 总结参考 在 Java 中,指令重排是一种性能优化技术,它涉及到编译器和处理器对程序中指令的执行顺序进行调整,以提高执行效率。 什么是指令重排序
指令重排序是一种在编译器和处理器级别发生的优化过程它改变了程序原有的指令执行顺序。这种优化可以在多个层面上发生包括编译器优化、即时编译优化JIT以及处理器层面的优化。
编译器优化
当 Java 代码被编译成字节码时Java 编译器可能会重新排列指令的顺序。这种重排序基于以下原则
独立性如果两个指令之间没有直接的数据依赖关系编译器可能会改变它们的顺序。性能提升重排序旨在优化程序的执行例如通过减少指令之间的延迟或改善分支预测。内存访问优化编译器可能会重新排列内存访问指令以减少缓存未命中的情况。
JIT 编译优化
Java 运行时的即时编译器JIT进一步优化已经编译的字节码。JIT 在程序执行时进行优化因此它能够根据当前的执行上下文和运行时信息进行更精细的优化。例如
基于热点代码的优化JIT 会识别程序中的热点频繁执行的代码区域并对这些区域进行专门优化。动态分析JIT 能够根据程序的实时性能数据调整优化策略。
处理器优化
现代处理器在执行指令时也会进行自己的重排序。这是为了更有效地利用处理器资源如执行单元、寄存器和缓存。处理器级的指令重排序基于以下原则
并行执行处理器会尝试并行执行多个独立的指令以提高执行效率。流水线优化处理器使用流水线技术来执行指令。通过重排序处理器可以减少流水线阻塞和等待时间。数据依赖性和冒险处理器会分析指令之间的数据依赖性确保重排序不会影响程序的正确执行。
重排序数据依赖性
如果两个操作访问同一个变量且这两个操作中有一个为写操作此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型
名称示例说明写后读a1;ba;写一个变量后再读这个变量写后写a1;a2;写一个变量后再写这个变量读后写ab;b1;读一个变量后再写这个变量
上面三种情况只要重排序两个操作的执行顺序程序的执行结果就会被改变。
硬件层的内存屏障
Intel硬件提供了一系列的内存屏障主要有:
lfenceload fence读屏障指令。在lfence指令前的读操作必须在lfence指令后的读操作前完成。即读串行化。sfencesave fence写屏障指令。在sfence指令前的写操作必须在sfence指令后的写操作前完成。即写串行化。mfencemodify/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中提供了四类内存屏障指令:
屏障类型指令示例说明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指向刚分配的内存地址此时instancenull但是对象还没有初始化完成
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 内存模型二——重排序