
44-happens-before规则引言前几篇我们讲了主内存与工作内存的抽象、原子性/可见性/有序性三大特性以及volatile的内存语义。但这些机制散落在各处——volatile管可见性、synchronized管原子性、final管初始化安全——它们之间有没有一个统一的推理框架让我们能判断这段代码的执行结果是否有保障happens-before就是JMM给出的答案。它是JSR-133JDK 5引入的核心概念定义了操作之间的偏序关系如果A happens-before B那么A的操作结果对B可见且A的执行顺序在B之前。happens-before不是描述物理时间上的先后而是JMM提供的一种可见性保证——只要程序员遵循happens-before规则编译器和CPU就可以自由优化JMM保证结果正确。本篇逐一拆解happens-before的八大规则每条配代码示例最后用传递性把它们串联起来展示如何用happens-before推理并发程序的正确性。happens-before的本质在讲具体规则前先澄清happens-before到底是什么。偏序关系而非全序happens-before是一种偏序关系partial ordering不是全序。这意味着对于有happens-before关系的两个操作JMM保证前者的结果对后者可见对于没有happens-before关系的两个操作JMM不提供任何保证——它们可能重排、可能不可见、可能数据竞争操作A ──hb── 操作B → A的结果对B可见A先于B 操作A 操作B → 无关系结果不确定数据竞争与时间先后的区别这是一个关键且常被误解的点。假设物理时间上操作A在10:00执行操作B在10:01执行如果A happens-before B那么A的结果必然对B可见如果A不happens-before B即使A在物理时间上先执行A的结果也不一定对B可见反过来也成立A happens-before B不代表A的物理执行时间一定早于B——编译器和CPU可能重排它们只要JMM保证的可见性不破坏即可。happens-before是逻辑上的保证不是物理时间的描述。与重排序的关系happens-before和重排序是对立统一的JMM允许编译器、CPU重排序前提是不破坏happens-before关系如果两个操作有happens-before关系不能重排到违反这个关系的顺序如果两个操作没有happens-before关系可以任意重排这就是JMM的设计哲学给程序员一组规则happens-before只要代码遵循这些规则JMM就保证可见性正确在规则之外的自由度内JVM和硬件可以尽情优化。规则一程序顺序规则程序顺序规则Program Order Rule在一个线程内按照代码的控制流顺序不是字节码顺序而是语义上的顺序前面的操作happens-before后面的操作。这是最直观的规则——单线程内代码写的顺序就是happens-before的顺序。但要注意这里的控制流顺序指语义顺序编译器仍然可以重排只要重排后单线程语义不变as-if-serial。// 适用 JDK 8/11/17publicclassProgramOrderDemo{privateintx0;privateinty1;publicvoidmethod(){x1;// 操作Ay2;// 操作Bintzxy;// 操作C// 程序顺序规则A hb BB hb CA hb C传递性// 所以 z 一定是 3不会是 1 或 2}}上面单线程内A happens-before BB happens-before C。注意编译器可能把x1和y2重排它们无数据依赖但不会把zxy重排到前面——因为C依赖A和B。程序顺序规则保证了单线程语义的正确性。关键点程序顺序规则只作用于同一个线程内。跨线程时这个规则不提供任何保证必须依赖其他规则。规则二监视器锁规则监视器锁规则Monitor Lock Rule对一个锁的unlock操作happens-before随后对同一个锁的lock操作。这条规则是synchronized可见性的基石。线程A释放锁前的所有写操作对随后获取同一把锁的线程B全部可见。// 适用 JDK 8/11/17publicclassMonitorLockDemo{privateintsharedData0;privatefinalObjectlocknewObject();publicvoidwriter(){synchronized(lock){sharedData42;// 操作A在锁内写}// 操作Bunlock}publicvoidreader(){synchronized(lock){// 操作Clock与B是同一把锁// 监视器锁规则B(unlock) hb C(lock)// 又因程序顺序规则A hb BC hb D// 传递性A hb D所以这里一定能读到 42System.out.println(sharedData);// 操作D}}}推演链路程序顺序规则A(写42) hb B(unlock)监视器锁规则B(unlock) hb C(lock)前提是同一把锁程序顺序规则C(lock) hb D(读)传递性A hb D所以reader一定能读到42。这就是synchronized保证可见性的底层逻辑。注意同一个锁如果writer和reader用不同的锁对象监视器锁规则不成立sharedData的可见性无保证。这是新手常犯的错误——以为加了synchronized就行却用了不同的锁对象。规则三volatile规则volatile规则Volatile Variable Rule对一个volatile变量的写操作happens-before后续对这个变量的读操作。上一篇详细讲了volatile的内存语义和屏障实现这里从happens-before角度再审视。volatile规则建立了跨线程的可见性保证线程A写volatile变量线程B读同一变量则A的写对B可见。// 适用 JDK 8/11/17publicclassVolatileRuleDemo{privateintdata0;// 普通变量privatevolatilebooleanreadyfalse;// volatile变量publicvoidwriter(){data42;// 操作A写普通变量readytrue;// 操作B写volatile变量}publicvoidreader(){if(ready){// 操作C读volatile变量// volatile规则B(写ready) hb C(读ready)// 程序顺序A hb BC hb D// 传递性A hb D所以这里一定读到 42System.out.println(data);// 操作D读普通变量}}}这就是volatile发布能力的来源。虽然data是普通变量但因为ready是volatile且data42在readytrue之前程序顺序规则线程B读到readytrue后data也必然是42。这个模式叫volatile发布模式常用于状态标志位volatile boolean running一次性安全发布DCL单例的volatile instance配置快照发布规则四线程启动规则线程启动规则Thread Start Rule主线程A执行ThreadB.start()操作happens-before线程B中的任意操作。这条规则保证了主线程在启动子线程之前的所有写操作对子线程启动后全部可见。子线程不会看到半初始化的状态。// 适用 JDK 8/11/17publicclassThreadStartDemo{privateintconfig0;privateMapString,Stringcontext;publicvoidstartWorker(){config42;// 操作A主线程写contextnewHashMap();// 操作B主线程写context.put(key,value);// 操作C主线程写ThreadworkernewThread(()-{// 线程启动规则start() hb 线程内任何操作// 传递性A/B/C hb start()start() hb D// 所以 A/B/C hb D子线程一定能看到 config42 和完整的contextSystem.out.println(config);// 操作DSystem.out.println(context.get(key));// 操作E});worker.start();// 操作Fstart()// 程序顺序A hb B hb C hb F// 线程启动规则F hb D, F hb E// 传递性A/B/C hb D/E}}为什么需要这条规则想象如果没有它主线程初始化了一堆数据然后启动子线程处理。如果子线程看不到主线程的初始化结果就会读到默认值0、null导致NPE或逻辑错误。线程启动规则保证了主线程到子线程的数据传递是安全的。实践含义把数据准备放在start()之前无需额外同步子线程就能看到。这比把数据通过volatile或锁传递更简洁。规则五线程终止规则线程终止规则Thread Termination Rule线程中的所有操作happens-before对该线程的Thread.join()成功返回或Thread.isAlive()返回false。这是线程启动规则的对称规则——启动规则保证主线程的数据对子线程可见终止规则保证子线程的数据对主线程等待join的线程可见。// 适用 JDK 8/11/17publicclassThreadTerminationDemo{privateintresult0;publicvoidrunAndJoin()throwsInterruptedException{ThreadworkernewThread(()-{resultcompute();// 操作A子线程写});worker.start();worker.join();// 操作B主线程join返回// 线程终止规则A hb B// 所以这里一定能读到 result 的最新值System.out.println(result);// 操作C主线程读}privateintcompute(){// 模拟耗时计算return42;}}如果没有这条规则主线程join返回后可能读不到子线程的写入——子线程的result42可能还在子线程的工作内存中未同步到主内存。线程终止规则强制JVM在join返回前把子线程的所有写操作同步到主内存。Thread.isAlive()的语义规则也覆盖isAlive()返回false的情况。一旦线程死亡它生前的所有操作对检测到它死亡的线程可见。但实践中用join()更可靠isAlive()轮询不推荐。规则六线程中断规则线程中断规则Thread Interruption Rule线程A调用线程B的interrupt()操作happens-before线程B检测到中断事件通过InterruptedException或Thread.interrupted()/isInterrupted()。这条规则保证了中断信号的可见性——A发出中断后B一定能感知到。// 适用 JDK 8/11/17publicclassInterruptionDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadworkernewThread(()-{try{while(!Thread.currentThread().isInterrupted()){// 正常工作Thread.sleep(100);}// 检测到中断interrupt() hb 这里System.out.println(通过isInterrupted检测到中断);}catch(InterruptedExceptione){// sleep被中断会抛异常并清除中断标志// interrupt() hb catch块System.out.println(通过InterruptedException检测到中断);}});worker.start();Thread.sleep(500);// 主线程发出中断worker.interrupt();// 操作X// 中断规则X hb worker检测到中断}}中断规则的实践意义不要用自定义的volatile标志位替代中断。有些开发者自己搞volatile boolean stopped却忽略了Thread.interrupt()不仅能传递中断信号还能唤醒阻塞在sleep/wait/join上的线程。自定义标志位无法唤醒阻塞且需要额外处理可见性。中断机制是Java推荐的协作式取消方案。规则七对象终结规则对象终结规则Finalizer Rule一个对象的构造函数执行结束happens-before它的finalize()方法的开始。这条规则保证了对象在进入finalize前构造函数的所有初始化都已完成。即使GC在构造函数刚结束后立即回收finalize也能看到完整的对象状态。// 适用 JDK 8/11/17publicclassFinalizerRuleDemo{privateintvalue;publicFinalizerRuleDemo(intvalue){this.valuevalue;// 操作A构造函数内写}// 构造函数结束 // 操作B// 对象终结规则B hb finalize开始Overrideprotectedvoidfinalize()throwsThrowable{// 对象终结规则保证构造函数已完成// 这里 value 一定是构造时传入的值System.out.println(Finalizing, valuevalue);// 操作Csuper.finalize();}publicstaticvoidmain(String[]args){newFinalizerRuleDemo(42);// 创建后立即丢弃System.gc();// 建议GC不保证立即执行}}但请注意finalize()从JDK 9开始被标记为DeprecatedJDK 18正式为forRemoval。现代Java应该用CleanerJDK 9或try-with-resources替代finalize。这条happens-before规则更多是规范层面的完整性实践中应避免依赖finalize。对象终结规则的意义在于规范自洽——如果JVM要调用finalize就必须保证构造函数的结果对它可见。但finalize的执行时机不确定、甚至可能不执行所以这条规则的实际应用价值最低。规则八传递性传递性Transitivity如果A happens-before B且B happens-before C那么A happens-before C。传递性是把前七条规则串联起来的粘合剂。单独看每条规则它们只覆盖特定场景有了传递性我们可以构建跨规则、跨机制的推理链。// 适用 JDK 8/11/17publicclassTransitivityDemo{privateinta0;// 普通变量privateintb0;// 普通变量privatevolatilebooleanflagfalse;// volatile// 线程1publicvoidwriter(){a1;// 操作A1写普通变量b2;// 操作A2写普通变量flagtrue;// 操作A3写volatilevolatile规则锚点}// 线程2publicvoidreader(){if(flag){// 操作B1读volatileintr1a;// 操作B2读普通变量intr2b;// 操作B3读普通变量// 推理// 程序顺序A1 hb A3, A2 hb A3// volatile规则A3 hb B1// 程序顺序B1 hb B2, B1 hb B3// 传递性A1 hb B2, A2 hb B3// 所以 r11, r22 必然成立System.out.println(ar1, br2);}}}这是最典型的传递性应用用volatile变量作为发布锚点把前面的普通写和后面的普通读通过传递性关联起来。这正是DCL单例、安全发布模式的核心原理。传递性的威力传递性让我们可以组合不同的同步机制// 适用 JDK 8/11/17// 组合 synchronized 和 volatile 的场景publicclassCombinedSyncDemo{privateintdata0;privatevolatilebooleanreadyfalse;privatefinalObjectlocknewObject();// 线程1synchronized写publicvoidwriter(){synchronized(lock){data42;// 操作A}// 操作Bunlock}// 线程2先获取同一把锁再写volatilepublicvoidpublisher(){synchronized(lock){// 操作Clock// 监视器锁规则B(unlock) hb C(lock)// 程序顺序A hb BC hb D// 传递性A hb D// 所以这里 data 一定是 42System.out.println(data);// 操作D}readytrue;// 操作E写volatile}// 线程3读volatile后读datapublicvoidreader(){if(ready){// 操作F读volatile// volatile规则E hb F// 传递性链A hb B hb C hb D hb E hb F hb GSystem.out.println(data);// 操作G读到42}}}虽然实际开发中很少这样混用但这个例子展示了传递性如何让不同规则协同工作。只要能画出一条完整的happens-before链就能推导出可见性保证。用happens-before推理数据竞争happens-before的最终用途是判断程序是否有数据竞争Data Race。JMM的定义是如果一个程序的所有多线程访问都有happens-before关系覆盖则该程序是正确同步的否则存在数据竞争执行结果不确定。推理步骤列出所有共享变量的访问找出跨线程读写的变量标注每个访问的线程和操作类型读/写检查每对冲突访问是否有happens-before关系冲突指至少一个是写如果所有冲突访问都有hb覆盖→正确同步结果确定如果有冲突访问无hb覆盖→数据竞争结果不确定反例无同步的共享访问// 适用 JDK 8/11/17 —— 反例存在数据竞争publicclassDataRaceDemo{privateintx0;// 普通变量无同步publicvoidwriter(){x1;// 线程A的写}publicvoidreader(){intrx;// 线程B的读// writer和reader之间无happens-before关系// r 可能是 0 或 1结果不确定}}这里writer的写和reader的读之间没有任何happens-before关系——不满足程序顺序不同线程、不满足volatilex不是volatile、不满足监视器锁无synchronized。所以存在数据竞争结果不确定。修复方法给x加volatile启用volatile规则、加synchronized启用监视器锁规则、或用AtomicInteger。实践要点happens-before是推理工具不是API。你不能在代码里调用happens-before它是JMM规范层面的概念帮助你判断代码是否正确同步。养成写并发代码时画hb链的习惯能避免大量隐蔽bug。不要混淆happens-before和时间先后。物理时间上A先执行不代表A hb BA hb B也不代表A物理上先执行。happens-before是可见性保证不是时间描述。面试中这个区分经常考。同一把锁是监视器锁规则的前提。两个synchronized块用不同的锁对象不构成happens-before关系。这是生产事故的高发点——开发者以为都加了synchronized就行却用了不同锁。volatile发布模式要成对使用。写线程把普通写放在volatile写之前读线程把普通读放在volatile读之后才能用传递性建立hb链。如果普通写在volatile写之后或普通读在volatile读之前链路就断了。线程启动/终止规则是最常被忽略的免费同步。主线程在start()前准备的数据子线程天然可见子线程join()后的数据主线程天然可见。不需要额外同步。善用这两条规则能简化大量代码。finalize不可依赖。虽然对象终结规则存在但finalize的执行时机和是否执行都不确定。JDK 9已废弃finalize改用Cleaner或try-with-resources。不要把对象终结规则作为正确性依赖。happens-before不保证复合操作原子性。即使每个单独的volatile读写有hb关系volatile i依然不安全——因为hb保证的是可见性不是原子性。复合操作仍需synchronized或原子类。用JCStress验证happens-before推理。当你不确定某段并发代码是否有数据竞争时用JCStressJVM并发压力测试工具能帮你发现可见性bug。它通过海量迭代结果统计揭示那些理论上可能出错的竞态。小结happens-before是JMM的统一推理框架定义操作间的偏序关系保证前者结果对后者可见。它是逻辑保证不是物理时间描述八大规则程序顺序单线程内、监视器锁unlock hb lock、volatile写 hb 读、线程启动start hb 线程内操作、线程终止线程操作 hb join返回、中断interrupt hb 检测中断、对象终结构造结束 hb finalize、传递性A hb B且B hb C则A hb C传递性是粘合剂把不同规则串联构建跨机制的可见性链路。volatile发布模式普通写→volatile写→volatile读→普通读就是传递性的经典应用数据竞争判断所有冲突的跨线程访问都有hb覆盖→正确同步否则存在数据竞争结果不确定实践核心写并发代码时画hb链确认可见性有保障同一把锁/同一个volatile变量是规则生效的前提线程启动和终止规则是免费的跨线程同步善加利用