
文章目录1. 问题与场景多线程下为何出现「看不见、顺序乱」2. JMM 基础主内存与本地内存的抽象3. Happens-Before约定「何时可见」4. volatile可见性与禁止重排5. 管程与 synchronized互斥与块内可见性6. final 域不可变对象的安全发布7. 实际开发与架构设计中的注意点总结与参考链接以库存扣减为例——多线程同时扣减同一库存时会出现「扣完了别的线程还读到旧值」导致超卖或「先判断库存再扣减」被重排成「先扣减再判断」导致逻辑错乱。根因是 CPU 缓存和指令重排写操作未必立刻对其它线程可见代码顺序也未必等于执行顺序。JMM 用主内存 / 本地内存抽象和Happens-Before规则约定「何时对另一线程可见」再通过volatile如扣减完成标志、synchronized扣减块互斥、final库存上限等配置的安全发布把规则落地。核心就一句先有「何时可见」的约定Happens-Before再用关键字和锁去实现业务才能安全地写多线程共享变量。下文按「现象与根因 → JMM 与 Happens-Before → volatile/synchronized/final 用法与边界 → 实际开发注意点」展开文末总结与参考链接。1. 问题与场景多线程下为何出现「看不见、顺序乱」多线程共享变量时常出现两类现象写完了别的线程看不见可见性、代码顺序和执行顺序不一致有序性于是出现脏读、重复扣减、状态不一致等难以复现的 bug。根因简要可见性CPU 有缓存写往往先到缓存再异步刷主存其他线程读到的可能是旧缓存。有序性编译器/CPU 会重排指令在不改变单线程语义的前提下打乱顺序多线程下可能把「先读后写」重排成「先写后读」从而读到未初始化的值。若没有统一规范既无法判断「何时对另一线程可见」也无法在优化与正确性之间做权衡。因此需要一套约定「何时可见」的规范并让具体的关键字和锁去实现它——这正是下文第 2、3 节要讲的 JMM 与 Happens-Before以及第 4、5、6 节的 volatile/synchronized/final2. JMM 基础主内存与本地内存的抽象要约定「何时对另一线程可见」先要对「谁在哪儿、何时同步」有一层统一描述。JMM 用「主内存线程本地内存」的抽象来做到这一点共享变量在主内存每个线程有一份「本地内存」涵盖缓存、写缓冲区、寄存器以及编译器重排等读/写先与本地内存交互再与主内存同步。JMM 通过控制「主内存与各线程本地内存之间的交互时机」来提供内存可见性保证。在这种抽象下线程间通信被规范为两步① 线程 A 将本地内存中更新过的共享变量刷新到主内存② 线程 B从主内存读取A 已更新过的共享变量。也就是说可见性由「何时刷新、何时读」的规则决定——这套规则就是下一节的 Happens-Before。3. Happens-Before约定「何时可见」上一节说到可见性由「何时刷新、何时读」的规则决定在 JMM 里这套规则就是Happens-Before若A Happens-Before B则 A 的结果对 B 可见。它约束的是可见性顺序而不是执行顺序——允许不相关操作重排但不允许破坏 Happens-Before 关系这样既满足业务对可见性的需求又给编译器和 CPU 留出优化空间。常用规则与使用场景规则含义典型场景程序顺序同一线程内前面的修改对后面可见单线程内逻辑顺序可推理volatile 变量对 volatile 的写对后续该变量的读可见标志位、开关如「x 已写好」用 vtrue 表示传递性A→BB→C ⇒ A→C组合多条规则推导跨线程可见性管程中锁解锁 Happens-Before 后续对该锁的加锁synchronized块内修改对下一进入该锁的线程可见线程 start()主线程 start 前操作对子线程可见传给子线程的初始参数、配置线程 join()子线程结束对 join 返回后的主线程可见子线程计算结果对主线程可见线程中断interrupt() 先于被中断线程检测到中断中断请求一定早于中断处理示例volatile 做可见性桥梁classVolatileExample{intx0;volatilebooleanvfalse;publicvoidwriter(){x42;// 程序顺序先于 vtruevtrue;// volatile 写对后续读 v 可见}publicvoidreader(){if(vtrue){// 传递性 ⇒ x42 对当前线程可见// x 在 1.5 上等于 42}}}上面用v做「x 已写好」的信号volatile 写与读建立 Happens-Before再通过传递性把x42的可见性带给 reader无需锁整块代码。volatile 正是实现「对 volatile 变量的写对后续读可见」这条 Happens-Before 规则的关键字下一节单独说它的语义与边界。4. volatile可见性与禁止重排Happens-Before 里有一条是「对 volatile 的写对后续该变量的读可见」volatile关键字就是实现这条规则的手段。语义对该变量的读写与主内存直接交互不经过线程本地缓存可见性。禁止对该变量相关操作的指令重排有序性。实现上通过读/写前后插入内存屏障保证。适用场景单变量状态、标志位写一次、多线程尽快可见如关闭开关、初始化完成标志。与 Happens-Before 配合做「可见性桥梁」如上节示例。边界与注意不保证原子性i 等复合操作仍需synchronized或原子类。仅限本变量只保证该变量本身及其前后顺序不保证其他共享变量的可见性需通过传递性组合规则。当需求不只是「单变量可见」而是「同一时刻只有一个线程执行某段代码」或「先检查再更新」这类复合操作时就要用到synchronized见下一节。5. 管程与 synchronized互斥与块内可见性volatile 只保证单变量可见性不保证互斥和原子性。需要互斥或「先读后改」的原子性时要用synchronized它对应 Happens-Before 里的「管程中锁」规则——解锁 Happens-Before 后续对该锁的加锁。语义管程Monitor通用同步原语synchronized是 Java 对管程的实现。JMM 规则解锁 Happens-Before 后续对该锁的加锁→ 块内对共享变量的写在释放锁后对下一个获得同一把锁的线程可见。适用场景需要互斥且块内修改对下一进入者可见如判断并更新库存、先检查再赋值。需要原子性的复合操作如 i、check-then-act。示例synchronized(this){if(this.x12){this.x12;}}// 解锁后下一个获得 this 锁的线程能看到 x12还有一种常见需求是「构造完成后不再变的配置或引用希望无锁地安全发布给多线程」——这由final配合 JMM 对 final 域的禁止重排规则来保证见下一节。6. final 域不可变对象的安全发布JMM 对final域做了特殊规定禁止把对 final 域的写重排到构造函数 return 之外从而在无锁的前提下实现「构造完成即对其它线程可见」的安全发布。语义基本类型 final禁止把对 final 域的写重排到构造函数 return 之外final 写之后、return 前插入 StoreStore 屏障→ 对象发布时 final 已写好。引用类型 final构造函数内对 final 引用所指对象的写不能与「把该引用赋给引用变量」重排 → 避免发布半初始化对象。适用场景构造完成后不再变的配置、常量、引用希望构造完成后对所有线程立即可见且无需加锁。不可变对象final 基本类型、String、枚举、Long/Double/BigInteger/BigDecimal 等一旦正确发布可被多线程安全使用AtomicInteger/AtomicLong 为可变需按原子类语义使用。7. 实际开发与架构设计中的注意点前面几节分别讲了 JMM 与 Happens-Before 的约定以及 volatile、synchronized、final 的语义与场景。实际开发中需要把这些串起来什么时候选谁、各有什么边界、出问题时怎么排查。下面按选型、边界与排查三块简要梳理。选型标准诉求选型说明单变量状态/标志位仅需可见性volatile轻量不保证复合操作原子性需要互斥或复合操作原子性、块内修改对下一进入者可见synchronized锁粒度尽量小避免全局锁构造后不变、安全发布final 正确构造无锁发布依赖 JMM 禁止重排边界与局限volatile不适用于 i、check-then-act 等复合操作多变量可见性需依赖 Happens-Before 传递性设计。synchronized需明确锁对象同一对象才互斥注意锁顺序避免死锁。final仅保证构造完成时的可见性构造后若通过其他引用修改对象内部状态仍需其他同步手段。问题排查思路现象多线程下偶尔读到旧值或未初始化值。定位先确认是否为可见性/有序性是否未用 volatile/synchronized/final或跨线程可见性未满足 Happens-Before再结合 Thread Dump 看是否阻塞、死锁。措施按诉求补 volatile单变量可见、synchronized互斥可见、或 final安全发布复核共享变量的读写是否都在同一锁或同一 Happens-Before 链上。总结与参考链接回到开头的库存扣减场景要避免「扣完了别人还读到旧值」导致的超卖以及「先判断再扣减」被重排导致的逻辑错乱就需要先理解「何时对另一线程可见」JMM 与 Happens-Before再把约定落到代码里volatile/synchronized/final。全文要点如下。总结现象与根因多线程下「看不见、顺序乱」来自 CPU 缓存与指令重排JMM 用主内存/本地内存抽象和 Happens-Before 约定「何时可见」。手段与边界volatile 做单变量可见与禁止重排不保证原子性synchronized 做互斥与块内可见final 做无锁安全发布。按诉求选型注意各手段的边界与组合时的 Happens-Before 关系。排查先确认是否为可见性/有序性未用或误用 volatile/synchronized/final再结合 Thread Dump 看阻塞与死锁按诉求补手段并复核 Happens-Before 链。参考链接主题链接volatile 深入java 并发关键字volatile 深入浅出synchronized 深入关键字synchronized 详解JMM 总览pdai - JMM