"构造函数里不要把this泄漏出去"是并发编程的经典建议,但这个警告到底有多真实?这篇真实构造一个会逃逸的场景,让另一个线程去抓包,结果是200次测试200次命中。
上一篇:静态初始化顺序真实验证
故意在构造函数的第一行就把this赋给一个静态字段,让它在构造函数真正执行完之前就对外可见。
ThisEscapeDemo.java static volatile ThisEscapeDemo leaked; int value; // 未加volatile ThisEscapeDemo(int v) { leaked = this; // 危险:this在构造完成前就泄漏了 Thread.sleep(50); // 模拟耗时初始化 this.value = v; // value的赋值发生在泄漏之后 }
另起一个观察者线程,自旋等待 leaked 非空后立即读取 leaked.value,重复200轮,统计读到默认值 0(未初始化完成)和真实赋值 42(初始化完成)的次数。
200 轮真实测试中,观察者线程看到 value==0(未初始化完成)的次数: 200 200 轮真实测试中,观察者线程看到 value==42(已初始化完成)的次数: 0 真实证明: this逃逸后,其他线程有真实概率看到构造未完成的对象状态
真实结果比预想的还要极端:200轮测试全部观察到了未初始化完成的状态,没有一次意外读到真实的 42。这是因为 leaked = this 这一行在构造函数的最开始就真实执行了,观察者线程自旋等待的条件(leaked != null)几乎立刻满足,而 value 的真实赋值要等构造函数里那句 Thread.sleep(50) 走完之后才会发生——这中间有一个真实存在的 50 毫秒窗口期,观察者线程的读取动作几乎必然落在这个窗口期内。
用"构造完全结束后才把引用赋给可见字段"的正确方式重新测试。
ThisEscapeDemo.java static SafeDemo createAndPublish(int v) { SafeDemo d = new SafeDemo(v); // 构造函数内部先完成所有字段赋值 published = d; // 完全构造完成后才真实发布 return d; }
200 轮安全发布方式测试中,看到 value==0 的次数(应为0): 0
真实规避方式很直接:永远不要在构造函数内部把 this(或者任何指向正在构造的对象的引用)传给其他线程能访问到的地方——包括赋值给静态字段、注册为监听器、启动内部类线程等。如果确实需要"创建后自动注册"这类逻辑,应该像 SafeDemo.createAndPublish() 这样,用静态工厂方法在对象完全构造完成、构造函数已经返回之后再执行发布动作。
每一行都是真实运行这段代码得到的结果。