【底层原理之旅—字节码指令重排序】|Java 开发实战

简介: 【底层原理之旅—字节码指令重排序】|Java 开发实战

前提概要


指令重排序有两类,编译器重排序处理器重排序(至于内存系统指令重排较为复杂不是本章重点)


重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境。 编译器重排序发生在编译期,处理器重排序发生在运行时。其实指令重排序的本意是提高程序并发效率,原则是重排序后的程序运行结果和单线程运行结果一致。(AS IF SERIAL)




指令重排的原因


  • 为什么指令重排序会提高程序并发效率呢?这里先理解一下CPU的最小调度单位是线程这个概念。
  • 一个CPU同时只能处理一个线程,在最初单核CPU的时候,是通过轮询的方式去完成多线程的,在线程之间完成上下文切换。


  • 现在都是多核CPU,其中每个CPU也是在轮询线程,只不过多核CPU并发效率更高了。



这就存在一个问题,CPU的运算速度要远快于对内存的操作,将工作内存数据写入主内存即物理内存时,如果两个CPU同时需要写入同一块内存区域,这就需要一个CPU等待另一个CPU写入完成后再写,这就造成了CPU的浪费,而这种情况在单核CPU是不存在的,所以需要指令重排序




举个例子


int a = 1;
int b = 2;
复制代码


指令排序案例分析


  • a和b需要写入不同的内存区域,在多线程中:


  • 如果CPU1是先写入a到内存a,再写入b到内存b。
  • 那么CPU2必然也是这个顺序,这就容易造成两个CPU想同时往内存a中写入,这就需要一个CPU等待另一个写入完成,这就造成了CPU的等待浪费。
  • 但是如果线程2中指令重排序一下,变为int b = 2;  int a = 1;
  • 那么CPU2就是先写入b到内存b,再写入a到内存a。
  • 这样两块CPU就可以同时写入,这才是真正的多核CPU,这就是指令重排序的目的。



指令排序的局限性


当然指令重排序也是有条件的,有一个语句间依赖性的概念,分为数据依赖性和控制依赖性。



数据依赖性


指后一条语句要使用上一条语句的数据,控制依赖性是指后一条语句要使用上一条语句的判断结果。语句间有依赖性就不可以指令重排序了,这也很好理解。但是在高并发中,如果不对共享变量做并发处理,指令重排序会造成严重问题。举个例子:

image.png



  • 如何线程a调用了write方法,并进行了指令重排序,先将b=true写入内存再将a=1写入内存,这就可能出现一种情况。
  • 线程a将b=true写入内存后还没有来得及将a=1写入内存,此时线程b正在调用read方法,由于从内存中读到的b为true。
  • if(b)判断成功后去读a,此时a并没有被写入为1,所以这时候共享变量就发生的错误。



可见性


可以用volatile关键字去修饰共享变量,这时候如果该变量在工作内存中被修改,那么不需要写入主内存就对其他线程是可见的,即保证了该变量的可见性


例如同步锁保证得到锁时从内存里重新读入数据刷新缓存,释放锁时将数据写回内存以保数据可见,而volatile变量干脆都是读写内存。


有序性


  • 同时被volatile修饰的变量不允许被指令重排序和缓存(内存屏障)。


  • 避免多线程中指令重排序问题需要我们的编码遵循happen-and-before原则,都是有关于并发编程加锁机制


  1. unlock操作,先行发生于对同一对象的lock操作,这里包括发生在其它线程中的lock操作。
  2. volatile修饰的变量写先发生于读,这保证了该变量的可见性。
  3. thread的start()方法先行发生于该线程的每一个动作。
  4. thread的join()方法即终止方法后发生先于该线程的每一个动作,可以用thread.alive()方法的返回值判断该线程是否已经终止。
  5. thread的interrupte()方法即中断方法先行发生于被中断线程的代码检测到中断事件的发生,通过thread.interrupte()方法检测线程是否已中断。
  6. 一个对象的初始化完成即其构造方法的调用结束要先行与他的终结方法例如finalize()。
  7. 动作有传递性,如果动作A先于动作B,动作B先于动作C,那么动作A先于动作C。

在并发程序中,程序员会特别关注不同进程或线程之间的数据同步,特别是多个线程同时修改同一变量时,必须采取可靠的同步或其它措施保障数据被正确地修改,这里的一条重要原则是:不要假设指令执行的顺序,你无法预知不同线程之间的指令会以何种顺序执行。


理想的模型是:各种指令执行的顺序是唯一且有序的,这个顺序就是它们被编写在代码中的顺序,与处理器或其它因素无关,这种模型被称作顺序一致性模型,也是基于冯·诺依曼体系的模型。


当然,这种假设本身是合理的,在实践中也鲜有异常发生,但事实上,没有哪个现代多处理器架构会采用这种模型,因为它是在是太低效了。而在编译优化和CPU流水线中,几乎都涉及到指令重排序。




编译期重排序


编译期重排序的典型就是通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数,充分复用寄存器的存储值。


  • 第一条指令计算一个值赋给变量A并存放在寄存器中,
  • 第二条指令与A无关但需要占用寄存器(假设它将占用A所在的那个寄存器)
  • 第三条指令使用A的值且与第二条指令无关。
  • 那么如果按照顺序一致性模型,A在第一条指令执行过后被放入寄存器,第二条指令执行时A不再存在,第三条指令执行时A重新被读入寄存器,而这个过程中,A的值没有发生变化
  • 通常编译器都会交换第二和第三条指令的位置,这样第一条指令结束时A存在于寄存器中,接下来可以直接从寄存器中读取A的值,降低了重复读取的开销



重排序对于流水线的意义


现代CPU几乎都采用流水线机制加快指令的处理速度,一般来说,一条指令需要若干个CPU时钟周期处理,而通过流水线并行执行,可以在同等的时钟周期内执行若干条指令,具体做法简单地说就是把指令分为不同的执行周期,例如读取、寻址、解析、执行等步骤,并放在不同的元件中处理,同时在执行单元EU中,功能单元被分为不同的元件,例如加法元件、乘法元件、加载元件、存储元件等,可以进一步实现不同的计算并行执行。


流水线架构决定了指令应该被并行执行,而不是在顺序化模型中所认为的那样。重排序有利于充分使用流水线,进而达到超标量的效果。




相关文章
|
16天前
|
安全 Java API
JAVA并发编程JUC包之CAS原理
在JDK 1.5之后,Java API引入了`java.util.concurrent`包(简称JUC包),提供了多种并发工具类,如原子类`AtomicXX`、线程池`Executors`、信号量`Semaphore`、阻塞队列等。这些工具类简化了并发编程的复杂度。原子类`Atomic`尤其重要,它提供了线程安全的变量更新方法,支持整型、长整型、布尔型、数组及对象属性的原子修改。结合`volatile`关键字,可以实现多线程环境下共享变量的安全修改。
|
11天前
|
算法 Java
JAVA并发编程系列(8)CountDownLatch核心原理
面试中的编程题目“模拟拼团”,我们通过使用CountDownLatch来实现多线程条件下的拼团逻辑。此外,深入解析了CountDownLatch的核心原理及其内部实现机制,特别是`await()`方法的具体工作流程。通过详细分析源码与内部结构,帮助读者更好地理解并发编程的关键概念。
|
11天前
|
Java
JAVA并发编程系列(9)CyclicBarrier循环屏障原理分析
本文介绍了拼多多面试中的模拟拼团问题,通过使用 `CyclicBarrier` 实现了多人拼团成功后提交订单并支付的功能。与之前的 `CountDownLatch` 方法不同,`CyclicBarrier` 能够确保所有线程到达屏障点后继续执行,并且屏障可重复使用。文章详细解析了 `CyclicBarrier` 的核心原理及使用方法,并通过代码示例展示了其工作流程。最后,文章还提供了 `CyclicBarrier` 的源码分析,帮助读者深入理解其实现机制。
|
4天前
|
安全 Java 编译器
Java反射的原理
Java 反射是一种强大的特性,允许程序在运行时动态加载、查询和操作类及其成员。通过 `java.lang.reflect` 包中的类,可以获取类的信息并调用其方法。反射基于类加载器和 `Class` 对象,可通过类名、`getClass()` 或 `loadClass()` 获取 `Class` 对象。反射可用来获取构造函数、方法和字段,并动态创建实例、调用方法和访问字段。虽然提供灵活性,但反射会增加性能开销,应谨慎使用。常见应用场景包括框架开发、动态代理、注解处理和测试框架。
|
11天前
|
Java
Java的aop是如何实现的?原理是什么?
Java的aop是如何实现的?原理是什么?
15 4
|
15天前
|
存储 Java
JAVA并发编程AQS原理剖析
很多小朋友面试时候,面试官考察并发编程部分,都会被问:说一下AQS原理。面对并发编程基础和面试经验,专栏采用通俗简洁无废话无八股文方式,已陆续梳理分享了《一文看懂全部锁机制》、《JUC包之CAS原理》、《volatile核心原理》、《synchronized全能王的原理》,希望可以帮到大家巩固相关核心技术原理。今天我们聊聊AQS....
|
12天前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
20天前
|
Java 开发者 数据格式
【Java笔记+踩坑】SpringBoot基础4——原理篇
bean的8种加载方式,自动配置原理、自定义starter开发、SpringBoot程序启动流程解析
【Java笔记+踩坑】SpringBoot基础4——原理篇
|
9天前
|
存储 缓存 Java
JAVA并发编程系列(11)线程池底层原理架构剖析
本文详细解析了Java线程池的核心参数及其意义,包括核心线程数量(corePoolSize)、最大线程数量(maximumPoolSize)、线程空闲时间(keepAliveTime)、任务存储队列(workQueue)、线程工厂(threadFactory)及拒绝策略(handler)。此外,还介绍了四种常见的线程池:可缓存线程池(newCachedThreadPool)、定时调度线程池(newScheduledThreadPool)、单线程池(newSingleThreadExecutor)及固定长度线程池(newFixedThreadPool)。
|
15天前
|
Java
JAVA并发编程ReentrantLock核心原理剖析
本文介绍了Java并发编程中ReentrantLock的重要性和优势,详细解析了其原理及源码实现。ReentrantLock作为一种可重入锁,弥补了synchronized的不足,如支持公平锁与非公平锁、响应中断等。文章通过源码分析,展示了ReentrantLock如何基于AQS实现公平锁和非公平锁,并解释了两者的具体实现过程。
下一篇
无影云桌面