《Java Concurrency in Practice》中Holder类的安全发布及并发疑问
咱们先从你给出的代码场景入手,一步步拆解核心疑惑:
首先贴出原问题中的核心代码片段,方便对照:
// 原Holder类 public class Holder { private int n; public Holder(int n) { this.n = n; } public void assertSanity() { if(n != n) throw new AssertionError("This statement is false."); } } // 不安全发布代码 public Holder holder; public void initialize() { holder = new Holder(42); }
一、为什么volatile/final引用能实现Holder的安全发布?
你之前的疑问是「volatile和final只影响引用本身,不影响对象状态」,这个点只对了一半——Java内存模型(JMM)对volatile引用和final引用的语义,不仅仅保证引用本身的可见性,还附带了对象构造的可见性保证:
对volatile引用的情况
当你把holder声明为volatile时:
// 安全发布:volatile引用 public volatile Holder holder; public void initialize() { holder = new Holder(42); }
JMM的「happens-before」规则会强制要求:线程A完成holder = new Holder(42)的赋值后,Holder构造过程中所有对n的写入操作,都会被同步到主内存,并且对后续读取holder的线程可见。简单说就是:对象构造的所有操作,都happens-before于任何线程读取该volatile引用的操作。其他线程拿到holder引用时,看到的一定是构造完成的n值,不会出现n未初始化的情况,自然不会触发断言错误。
对final引用的情况
如果holder是final字段且在构造阶段正确初始化(无this逸出):
// 安全发布:final引用 public final Holder holder; public YourClass() { // 在构造函数中初始化 holder = new Holder(42); }
JMM的final字段语义会保证:所有线程看到的final引用指向的对象,其构造过程已经完全完成,不会看到「半初始化」的Holder实例。哪怕n是非final字段,只要构造时没逸出this,也会保证构造完成后的状态对所有线程可见,因此不会出现n != n的诡异情况。
所以这两种方式确实能实现Holder的安全发布,不会抛出AssertionError。
二、把Holder的n改成volatile是否可行?和有效不可变性的关联?
先直接给结论:把n改成private volatile int n;确实能让Holder对不安全发布免疫,但这和原书中的「有效不可变性」思路本质不同:
原书中的方案:private final int n;(有效不可变性)
当n是final时,Holder对象一旦构造完成,状态就再也无法改变——这就是不可变对象的定义。不可变对象天生线程安全,哪怕是不安全发布,其他线程看到的要么是完全初始化后的对象,要么是null,绝不会看到半初始化的状态。这是最稳妥的方案,完全避免了状态变更的可见性与原子性问题。
用volatile修饰n的情况
// 修改后的Holder类 public class Holder { private volatile int n; public Holder(int n) { this.n = n; } public void assertSanity() { if(n != n) throw new AssertionError("This statement is false."); } }
volatile修饰n时,Holder的状态语法上是可变的(允许后续修改n),但volatile保证了n的所有读写操作都直接在主内存进行,且对所有线程可见。哪怕遇到不安全发布,线程读取n时,一定会拿到最新的、已完成赋值的42——因为构造函数里的this.n = n是volatile写,后续的volatile读会强制同步主内存的值,不会出现两次读取n结果不一致的情况,因此也不会触发断言。
但这种方案存在隐患:如果后续有代码对n做非原子操作(比如n++),volatile只能保证可见性,无法保证原子性,会出现线程安全问题;而final方案是真正的不可变,完全没有这类风险。所以final的方案更符合「有效不可变性」的设计思路,是推荐的做法。
三、原问题中AssertionError的本质原因
原场景中抛出错误的核心是:不安全发布导致线程看到了半初始化的Holder对象——JMM允许指令重排,构造函数里的this.n = n可能被排在holder = new Holder(42)之后,导致线程看到holder非空,但n还没被赋值(默认值0),第一次读n是0,第二次读时n可能已经变成42,最终触发0 != 42的断言。
而无论是用安全发布方式(volatile/final引用、静态初始化、锁保护),还是把n改成final/volatile,都是从「消除半初始化对象的可见性」角度解决问题:
- 安全发布方式:保证线程看到的
holder引用一定是构造完成后的对象; - 修改
n为final/volatile:哪怕看到半初始化的对象,n的读写也能保证可见性,不会出现前后读取不一致的情况。
内容的提问来源于stack exchange,提问作者88mariusz

