重排序跟有序性什么关系

发布时间:2026/8/20 22:41:17
重排序跟有序性什么关系 一句话概括有序性是我们要的目标程序按代码顺序执行重排序是可能破坏这个目标的原因指令被重新排列。什么是重排序重排序是指编译器和处理器为了优化性能改变程序指令的执行顺序但保证单线程下执行结果不变。三个层面的重排序publicclassReorderLevels{// 示例代码publicvoidexample(){inta1;// 语句1intb2;// 语句2intcab;// 语句3依赖a和b}}层面执行者做了什么示例编译器重排序Java编译器javac编译时重新安排指令顺序可能先编译语句2再编译语句1指令级并行重排序CPU处理器同时执行多条没有依赖的指令同时执行语句1和语句2内存系统重排序内存/cache系统由于缓存导致写操作延迟可见语句1的写入对其他CPU延迟可见⚖️有序性 vs 重排序关系对比表概念定义关注点类比有序性程序执行的顺序符合代码书写顺序结果正确性按菜谱步骤做菜重排序实际执行时指令被重新排列执行效率厨师同时准备多个菜的材料关键关系我们期望的顺序有序性目标 代码顺序 → 编译后顺序 → CPU执行顺序 → 内存可见顺序 但可能发生的重排序现实 代码顺序 ↛ 编译后顺序 ↛ CPU执行顺序 ↛ 内存可见顺序 ↓ ↓ ↓ 编译器重排 处理器重排 内存重排通过代码彻底理解示例1单线程下的重排序安全publicclassSingleThreadReorder{privateinta0;privateintb0;publicvoidwriter(){a1;// 语句Ab2;// 语句B// 单线程下即使A和B被重排序结果也是一样的// 因为这两个操作没有依赖关系}publicvoidreader(){System.out.println(aa, bb);// 总是看到 a1, b2单线程保证}}单线程黄金法则as-if-serial语义不管怎么重排序单线程程序的执行结果不能被改变。示例2多线程下的重排序问题危险publicclassMultiThreadReorderProblem{privateintx0;privateinty0;privatebooleanreadyfalse;// 线程1执行publicvoidwriter(){x42;// 操作1写xreadytrue;// 操作2写ready// 可能被重排序为先执行操作2再执行操作1}// 线程2执行publicvoidreader(){if(ready){// 操作3读readySystem.out.println(x x);// 操作4读x// 可能看到 x 0因为操作1可能在操作2之后执行}}}执行时序可能变成期望的顺序代码顺序 线程1: x42 → readytrue 线程2: 检查ready → 读取x 实际可能的顺序重排序后 线程1: readytrue → x42 ← 重排序发生了 线程2: 检查ready(true) → 读取x(0) ← 看到了未初始化的x重排序的三种类型详解1. 编译器优化重排序// 编译前代码publicclassCompilerReorder{publicintcalculate(){intaexpensiveComputation1();// 耗时操作1intbexpensiveComputation2();// 耗时操作2intcab;// 依赖a和bintdindependentComputation();// 独立操作returncd;}// 编译器可能重排序为publicintcalculateOptimized(){intaexpensiveComputation1();intdindependentComputation();// 提前执行独立操作intbexpensiveComputation2();// 和a并行执行如果支持intcab;returncd;// 结果相同但执行更快}}2. 指令级并行重排序CPU层面// 现代CPU可以同时执行多条指令publicclassCPUPipeline{publicvoidprocess(){// 以下操作没有依赖CPU可能并行执行intareadFromMemory1();// 内存读取慢intbreadFromMemory2();// 另一个内存读取intca10;// 计算需要aintdb*2;// 计算需要b// CPU执行可能// 周期1: 发起读取a、读取b// 周期2: 等待内存...同时可以做其他事情// 周期3: 计算c、计算d并行执行}}3. 内存系统重排序publicclassMemorySystemReorder{// 由于CPU缓存导致的重排序privateintdata0;privatebooleanflagfalse;publicvoidwriter(){data42;// 写入store buffer缓存不会立即到主内存flagtrue;// 可能先于data42对其他CPU可见}publicvoidreader(){if(flag){// 可能看到flagtrue但data还是0System.out.println(data);}}}内存重排序原因CPU1 CPU2 主内存 | | | 写data42 → Store Buffer data0 | | | 写flagtrue → 立即写入缓存 → 同步到主内存 → flagtrue | | | 稍后Store Buffer内容 → 主内存 → data42️如何保证有序性防止有害重排序方案1volatile关键字最常用publicclassVolatileEnsuresOrder{privateintx0;privatevolatilebooleanreadyfalse;// volatile防止重排序publicvoidwriter(){x42;// 普通写readytrue;// volatile写// 内存屏障禁止x42重排到readytrue之后}publicvoidreader(){if(ready){// volatile读System.out.println(x);// 保证看到x42// 内存屏障禁止其他操作重排到if之前}}}volatile的内存屏障// volatile写之前StoreStore屏障// volatile写之后StoreLoad屏障全能屏障// volatile读之后LoadLoad屏障 LoadStore屏障方案2synchronized同步块publicclassSynchronizedEnsuresOrder{privateintx0;privatebooleanreadyfalse;publicsynchronizedvoidwriter(){x42;readytrue;// synchronized保证块内不会重排到块外// 解锁时插入内存屏障}publicsynchronizedvoidreader(){if(ready){System.out.println(x);// 保证看到x42}}}方案3final关键字publicclassFinalEnsuresOrder{privatefinalImmutableObjectobj;// final保证安全发布publicFinalEnsuresOrder(){// 在构造函数内初始化this.objnewImmutableObject();// final写之后有内存屏障// 保证1. 构造函数内的写入不会重排到构造函数外// 2. 初始化完成才对其他线程可见}publicImmutableObjectgetObj(){returnobj;// 其他线程总是看到完全初始化的对象}}方案4Happens-Before规则publicclassHappensBeforeRules{// 规则1程序次序规则publicvoidprogramOrder(){intx1;// 操作Aintyx1;// 操作B保证看到x1}// 规则2锁规则publicvoidmonitorLock(){synchronized(this){x42;// 操作A}// 解锁操作Bsynchronized(this){// 操作C保证看到x42B happens-before CSystem.out.println(x);}}}重排序的利与弊好处为什么需要重排序publicclassBenefitsOfReorder{// 示例重排序带来的性能提升publicintcompute(){intareadFromDisk();// 磁盘IO很慢10msintbreadFromNetwork();// 网络IO很慢20msintclocalCalculation();// 本地计算很快0.01ms// 没有重排序10ms 20ms 0.01ms 30.01ms// 有重排序可能先执行localCalculation()同时发起磁盘和网络IO// 实际耗时 ≈ max(10ms, 20ms) 0.01ms 20.01ms// 性能提升33%returnabc;}}坏处导致的问题publicclassProblemsFromReorder{// 1. 双重检查锁问题privatestaticSingletoninstance;publicstaticSingletongetInstance(){if(instancenull){// 第一次检查synchronized(Singleton.class){if(instancenull){// 第二次检查instancenewSingleton();// 可能重排序// 分解为// memory allocate(); // 1.分配内存// instance memory; // 3.引用赋值可能提前// init(memory); // 2.初始化可能延后}}}returninstance;// 可能返回未初始化完成的对象}}实际检测重排序的代码经典的重排序检测程publicclassReorderDetector{privateintx0,y0;privateinta0,b0;publicvoiddetect()throwsInterruptedException{intiterations0;while(true){iterations;xyab0;Threadt1newThread(()-{// 可能重排序先执行xb再执行a1a1;xb;});Threadt2newThread(()-{// 可能重排序先执行ya再执行b1b1;ya;});t1.start();t2.start();t1.join();t2.join();// 如果没有重排序不可能出现x0且y0// 因为xb且ya而a和b至少有一个会被赋值为1if(x0y0){System.out.println(第iterations次检测到重排序);System.out.println(证明aa, bb, xx, yy);break;}if(iterations%100000){System.out.println(已测试 iterations 次未检测到重排序);}}}publicstaticvoidmain(String[]args)throwsInterruptedException{newReorderDetector().detect();}}这个程序证明了重排序的存在可能需要运行上万次才能检测到一次。内存屏障底层的顺序保证// 概念上的内存屏障类型publicclassMemoryBarriers{// LoadLoad屏障禁止后面的读操作重排到前面的读操作之前// LoadStore屏障禁止后面的写操作重排到前面的读操作之前// StoreStore屏障禁止后面的写操作重排到前面的写操作之前// StoreLoad屏障禁止后面的读操作重排到前面的写操作之前全能屏障// volatile的实现privatevolatileintv;publicvoidvolatileWrite(){// ...其他写操作// StoreStore屏障v1;// volatile写// StoreLoad屏障开销大但必要}publicvoidvolatileRead(){intlocalv;// volatile读// LoadLoad屏障 LoadStore屏障// ...后续操作}}总结重排序与有序性的核心关系一张图理解关系程序员的期望有序性 ↓ 代码顺序执行 ↓ ↙--- 编译器重排序 ---↘ ↙--- 处理器重排序 ---↘ ↙--- 内存系统重排序 -↘ ↓ 实际执行顺序可能乱序 ↓ 单线程结果正确as-if-serial 多线程可能出错 ↓ 需要同步原语建立happens-before关系 ↓ 保证多线程下的有序性关键要点重排序是优化手段为了性能编译器和CPU会重排指令单线程安全as-if-serial保证单线程结果正确多线程危险缺乏同步时重排序导致可见性问题有序性是目标我们需要程序按期望顺序执行同步是解决方案volatile、synchronized等保证多线程有序性一句话记住重排序是因执行时指令被重新排列有序性是果程序按代码顺序执行。我们需要用同步原语来防止有害的重排序从而保证多线程下的有序性。