【JVM原理详解】45-final域的内存语义 45-final域的内存语义引言上一篇的happens-before规则覆盖了volatile、锁、线程生命周期等场景但有一类特殊字段没有专门提及——final域。final在大多数开发者眼中只是不可变的语法约束但在JMM层面它承担着初始化安全性的关键职责。final的内存语义是JSR-133JDK 5重构的重要部分。在旧版Java内存模型中final的语义有缺陷——一个线程可能看到一个final字段的默认值0、null而非构造函数设置的值。JSR-133通过为final域引入专门的重排序规则和内存屏障修复了这个问题让final真正成为安全发布的基石。本篇深入final域的重排序规则写final、读final、初始化安全性保证、this逃逸陷阱、以及JDK 5的final语义增强用代码示例串联每一个知识点。final域为何需要特殊语义先理解问题为什么final不能像普通字段那样处理普通字段的初始化可见性问题考虑一个普通类的构造过程// 适用 JDK 8/11/17 —— 演示问题非final字段publicclassNormalFieldDemo{privateintx;// 普通字段publicNormalFieldDemo(intval){this.xval;// 构造函数内赋值}publicintgetX(){returnthis.x;}}如果线程A执行new NormalFieldDemo(42)线程B在线程A还没构造完时通过某种引用泄漏拿到对象引用并调用getX()线程B可能读到x0而非42。原因有二重排序构造函数内的this.x val和把对象引用发布出去之间没有同步编译器/CPU可能把赋值重排到发布之后可见性即使没有重排序this.x val可能还在线程A的工作内存未同步到主内存线程B读不到普通字段的初始化安全性需要程序员自己保证用volatile、锁、或final。final域的承诺final域的JMM语义就是为了解决上述问题只要对象在构造函数中没有逸出this不逃逸那么所有线程在构造函数完成后都能看到final字段的正确值——无需任何额外同步。这是JMM给予final的特权不用volatile、不用synchronized只要字段是final构造完成后就保证可见。这个保证叫做初始化安全性Initialization Safety。但这个保证有前提条件下面逐层展开。final域的重排序规则JMM为final域制定了专门的重排序规则通过内存屏障落地。规则分写final域和读final域两部分。写final域的重排序规则规则在构造函数内对final域的写入以及构造函数外把这个对象的引用赋值给某个引用变量这两个操作之间JMM禁止重排序。更准确地说写final域的操作不能重排序到构造函数之外。实现上JVM在写final域之后、构造函数结束之前插入一个StoreStore屏障[构造函数内] ... write final x 42 ← 写final域 StoreStore ───── ← 屏障禁止final写重排到构造函数外 [构造函数结束] 把对象引用发布给其他变量 ← 例如 instance objvolatile写或普通发布StoreStore屏障的作用保证final域的写先于对象引用发布完成。这样任何看到对象引用的线程都必然看到final域已被正确初始化。读final域的重排序规则规则初次读对象引用和初次读该对象的final域这两个操作之间JMM禁止重排序。实现上JVM在读对象引用之后、读final域之前插入一个LoadLoad屏障[某线程] read obj reference ← 初次读对象引用可能读到刚发布的对象 LoadLoad ───── ← 屏障禁止对象引用读和final域读重排 read final x ← 初次读final域 ...LoadLoad屏障的作用保证先读到正确的对象引用再读final域。如果没有这个屏障CPU可能先把final域的读预取到读引用之前读到的是旧对象的final值。完整屏障图示线程A构造并发布: ┌─────────────────────┐ │ write final x 42 │ ← 写final │ StoreStore ───── │ ← 屏障禁止final写逸出构造函数 │ [构造函数结束] │ │ publish ref to obj │ ← 发布引用赋值给共享变量 └─────────────────────┘ 线程B初次访问: ┌─────────────────────┐ │ read obj reference │ ← 读引用 │ LoadLoad ───── │ ← 屏障禁止final读提前 │ read final x │ ← 读final必然得到42 └─────────────────────┘初始化安全性重排序规则的最终目的是初始化安全性。这是final域内存语义的核心承诺。什么保证与什么不保证初始化安全性保证final字段对象构造完成后所有线程看到的final字段值都是构造函数设置的值不是默认值0/null通过final字段可达的对象如果final字段是引用类型且指向的对象在构造函数中被创建那么该可达对象的状态在构造完成后对所有线程可见初始化安全性不保证非final字段普通字段的初始化可见性不受保证除非额外同步final字段在构造后的修改final是引用类型时引用不可变但对象内容可变内容修改的可见性不受保证this逃逸场景如果在构造函数中this逃逸初始化安全性失效代码示例// 适用 JDK 5final语义增强后publicclassFinalSafetyDemo{finalinta;// final字段finalintb;// final字段intc;// 普通字段publicFinalSafetyDemo(){a1;// 写finalb2;// 写finalc3;// 写普通字段不在final保证范围内}// 假设某线程拿到已构造完成的 FinalSafetyDemo 引用publicvoidreadFields(){// a 和 b 的值保证是 1 和 2final初始化安全性System.out.println(aa, bb);// c 的值不保证——可能是 3也可能是 0// 需要额外同步volatile/锁才能保证 c 可见System.out.println(cc);}}关键区分final字段a、b有初始化安全保证普通字段c没有。这是final与普通字段在JMM层面的本质区别。final引用类型的可达性保证// 适用 JDK 5publicclassFinalReferenceDemo{finalint[]data;// final引用指向数组publicFinalReferenceDemo(){datanewint[10];for(inti0;i10;i){data[i]i*2;// 初始化数组内容}// 构造函数结束}publicvoidread(){// 初始化安全性保证// 1. data 引用本身可见指向那个int[10]// 2. data[0..9] 的值也可见通过final可达的对象for(inti0;i10;i){System.out.println(data[i]);// 保证读到 0,2,4,...,18}}}final引用类型不仅保证引用本身的可见性还保证构造函数中对引用对象内容的写入对其他线程可见。这是通过final可达的可见性传递。但注意如果在构造函数外修改了data[i]这些修改不受final保证——final只覆盖构造期写入。this逃逸final保证失效的陷阱初始化安全性有一个致命的前提对象在构造函数中不能逸出。如果this在构造函数完成前就泄露给其他线程final的保证失效。什么是this逃逸this逃逸指在构造函数执行期间this引用被其他线程获取的几种方式在构造函数中启动线程并把this传进去在构造函数中注册回调/监听器回调中引用this在构造函数中把this赋给静态变量或共享集合反例this逃逸导致final失效// 适用 JDK 8/11/17 —— 反例this逃逸publicclassThisEscapeDemo{finalintvalue;publicThisEscapeDemo(){value42;// 写final域// 危险在构造函数未结束时启动线程this逃逸newThread(()-{// 线程启动规则保证start() 之前的操作 hb 线程内操作// 但构造函数还未结束StoreStore屏障还没执行// 如果构造函数在 start() 后还有代码那些代码可能未执行System.out.println(valuevalue);// 可能读到0}).start();// 构造函数还有后续操作未结束// 此时另一个线程已经开始运行了}}这个例子有点微妙。表面上看线程启动规则保证start()之前的操作对子线程可见——但问题在于构造函数还没结束StoreStore屏障还没插入。更准确地说final的写虽然排在start()之前但JMM对final的保证是构造函数结束后而此时构造函数没结束。更明显的逃逸是注册回调// 适用 JDK 8/11/17 —— 反例通过监听器逃逸publicclassListenerEscapeDemo{finalintconfig;publicListenerEscapeDemo(EventSourcesource){source.registerListener(newMyListener(){OverridepublicvoidonEvent(){// 隐式持有 ThisEscapeDemo.thisSystem.out.println(config);// 可能读到0}});config42;// 写final在注册之后}}这里在构造函数中注册了监听器而监听器的回调可能在另一个线程触发。此时config的写还没执行写在注册之后回调读到的可能是默认值0。即使config是final也救不了——因为引用已经在构造函数完成前泄露final的初始化安全性前提被破坏。安全的构造模式避免this逃逸的几种方式// 适用 JDK 8/11/17// 方案1工厂方法 私有构造函数publicclassSafeFactory{privatefinalintvalue;privateSafeFactory(intvalue){this.valuevalue;// 构造函数完整结束}publicstaticSafeFactorycreate(EventSourcesource){SafeFactoryinstancenewSafeFactory(42);// 构造完整source.registerListener(instance::onEvent);// 构造后再注册returninstance;}privatevoidonEvent(){System.out.println(value);// 安全保证读到42}}// 方案2静态内部类持有状态 Effective Java 推荐publicclassHolderPattern{privatestaticclassListenerHolder{staticfinalEventListenerINSTANCEcreateListener();privatestaticEventListenercreateListener(){returnevent-{/* ... */};}}// 监听器在类初始化时创建JVM保证类初始化的线程安全}核心原则构造函数只做初始化不做发布——不启动线程、不注册回调、不赋给共享变量。所有发布动作放到构造函数之后。JDK 5的final语义增强final的内存语义并非一开始就健全。理解历史能加深对设计的理解。旧版JMM的问题JDK 5之前JSR-133之前的Java内存模型中final的语义有缺陷没有专门的final重排序规则final字段和普通字段一样读写没有特殊屏障初始化安全性缺失一个线程可能看到final字段是默认值0/null即使构造函数已设置值DCL单例不安全旧模型下即使加volatile也不能保证DCL安全因为final的初始化可见性没有保障旧模型的final只是语法层面的不可重新赋值没有内存语义。这让不可变对象在并发下并不真正安全。JSR-133的修复JSR-133JDK 5实现为final引入了完整内存语义写final的StoreStore屏障禁止final写重排到构造函数外读final的LoadLoad屏障禁止final读重排到对象引用读之前初始化安全性构造完成后final字段对所有线程可见前提无this逃逸final引用可达性通过final字段可达的对象内容在构造期写入的部分也可见这些增强让Java的不可变对象模式如String、Integer在并发下真正安全。String的不可变性之所以可靠正是因为final域的内存语义——String内部用finalchar[]JDK 9前或byte[]JDK 9存储字符多个线程并发读String永远不会看到半初始化状态。写final与读final的完整规则表JMM对final域的重排序规则可以归纳为下表结合上一篇volatile的规则表操作1 → 操作2普通读/写final写构造内volatile写volatile读初次读引用→读finalLoadLoad屏障写final→构造结束发布StoreStore屏障简化理解写final构造函数内final写 → StoreStore屏障 → 构造结束 → 发布引用读final读引用 → LoadLoad屏障 → 读final域final与volatile、synchronized的对比三者都能提供可见性保证但定位不同维度finalvolatilesynchronized可变性不可变构造后不可改可变可变保证范围初始化安全性构造期每次读写的可见性临界区内所有操作的可见性原子性开销仅构造时有屏障后续读几乎免费每次写有StoreLoad开销锁竞争上下文切换开销适用场景不可变对象、配置常量状态标志、单写多读复合操作、临界区保护前提条件this不逃逸单写或多写都行同一把锁设计原则能用final就用final——它是最轻量、最安全的可见性保证。需要可变状态时才考虑volatile或锁。不可变对象所有字段final是并发编程的最佳实践天生线程安全无需同步。final在实践中应用不可变对象模式final最常见的应用是构建不可变对象——所有字段final构造后状态不可变。// 适用 JDK 8/11/17// 不可变的配置快照publicfinalclassConfigSnapshot{privatefinalinttimeout;privatefinalintmaxConnections;privatefinalStringendpoint;privatefinalListStringservers;// final引用publicConfigSnapshot(inttimeout,intmaxConn,Stringendpoint,ListStringservers){this.timeouttimeout;this.maxConnectionsmaxConn;this.endpointendpoint;// 防御性拷贝保证内部List不可被外部修改this.serversList.copyOf(servers);// JDK 10返回不可修改的拷贝}// 只有getter没有setterpublicintgetTimeout(){returntimeout;}publicintgetMaxConnections(){returnmaxConnections;}publicStringgetEndpoint(){returnendpoint;}publicListStringgetServers(){returnservers;}// 已是不可变List}// 使用一个线程更新配置整体替换快照其他线程读取publicclassConfigManager{privatevolatileConfigSnapshotsnapshot;publicvoidupdateConfig(inttimeout,intmaxConn,Stringep,ListStringservers){// 构造完整的不可变对象后通过volatile发布snapshotnewConfigSnapshot(timeout,maxConn,ep,servers);}publicConfigSnapshotgetConfig(){returnsnapshot;// 读volatile引用拿到完整的快照}}这里final和volatile配合final保证ConfigSnapshot内部字段的初始化可见性volatile保证snapshot引用的发布可见性。两者组合实现了安全发布——读线程拿到引用时对象状态必然完整。为什么volatile final的组合如此强大构造函数内final的StoreStore屏障保证字段写先于构造结束发布时volatile的StoreStore屏障保证构造完成先于引用赋值读取时volatile的LoadLoad屏障保证先读引用再读字段final的LoadLoad屏障保证先读引用再读final域三层屏障叠加构成完整的可见性链路。这就是现代Java不可变对象volatile引用安全发布模式的底层原理。final字段的值与对象一个微妙之处final保证的是引用的可见性如果final字段是引用类型引用指向的对象的后续修改不在final保证范围内。// 适用 JDK 8/11/17publicclassFinalReferenceMutationDemo{finalListStringlistnewArrayList();// final引用publicFinalReferenceMutationDemo(){list.add(init);// 构造期写入final保证可见}publicvoidmutate(){list.add(after);// 构造后修改final不保证这次修改的可见性}publicvoidread(){// list引用保证可见指向那个ArrayList// init 保证可见构造期写入final可达// after 不保证可见——构造后的修改需要额外同步System.out.println(list);}}虽然list是final但list.add(after)在构造函数外执行这个修改的可见性不在final保证范围。如果多线程访问list仍需synchronized或用CopyOnWriteArrayList等并发集合。最佳实践final字段如果是集合尽量指向不可变集合List.copyOf、Collections.unmodifiableList彻底杜绝后续修改。实践要点不可变对象优先用final。所有字段声明为final的类是不可变对象天生线程安全无需任何同步。这是并发编程最简洁的安全模式——比volatile和锁都省心。警惕this逃逸。构造函数中不启动线程、不注册回调、不赋值给共享变量。所有发布动作放到构造函数之后。工厂方法是规避this逃逸的推荐模式。final引用类型要注意内部状态可变性。final ListString list保证list引用不变但list的内容可变。如果需要真正的不可变用List.copyOf或不可变集合包装。final volatile是安全发布的黄金组合。final保证对象内部状态在构造后可见volatile保证对象引用的发布可见。组合使用能彻底保证读线程拿到引用时对象状态必然完整。不要依赖旧版final语义。JDK 5之前final没有完整内存语义。现代代码基于JDK 8final语义是健全的。但如果维护遗留代码注意final在旧模型下可能不提供初始化安全性。final字段的反射修改是危险的。理论上可以通过反射Field.setAccessible(true)后set修改final字段但这会破坏JMM的final保证——其他线程可能看到修改前的值。除非特殊情况如反序列化框架不要反射改final。JDK 17的record类隐式final。record定义的类所有字段隐式final。这从语法层面强制了不可变性是构建值对象的现代方式。record类天然享受final的初始化安全性。final字段的StoreStore屏障在构造函数末尾。这意味着构造函数中途的final写如果配合this逃逸屏障可能还没生效。所以this逃逸的陷阱在final上也成立——final的保证以构造函数完整结束为前提。小结final域有专门的内存语义写final域禁止重排到构造函数外StoreStore屏障读final域禁止与初次读引用重排LoadLoad屏障。这是JDK 5JSR-133引入的增强初始化安全性只要对象在构造函数中未逸出所有线程都能看到final字段在构造期设置的值无需额外同步。这是final相对普通字段的核心特权final引用可达性通过final字段引用的对象在构造期写入的内容也对其他线程可见。但构造期外的修改不在保证范围——final保证引用不保证对象内容后续可变性this逃逸使final保证失效构造函数中泄露this启动线程、注册回调、赋值共享变量会破坏初始化安全性的前提。用工厂模式把发布动作移出构造函数JDK 5的final语义增强旧版JMM下final无专门屏障初始化可见性有缺陷。JSR-133修复后String等不可变类的并发安全性才真正有保障final volatile是安全发布模式final保证对象内部状态可见volatile保证引用发布可见组合使用实现完整的安全发布