bigywater.tech
Java · 178

final 字段:为什么构造完成后有特殊可见性

final 字段为什么在对象构造完成后有特殊的可见性保证?什么情况下保证会失效?这篇先用确定性的实验验证 this 逃逸,再看反射对 final 的影响,最后用 jcstress 在数据竞争发布下比较 final 与普通字段——并如实说明这个比较在本机没能区分两者(上一篇的内存序同理)。

fin = 0
this 逃逸时另一个线程读到的 final 字段
IllegalAccessException
反射修改 static final 与 record 字段
0 次
数据竞争发布下三种写法读到默认值的次数(本机)

this 逃逸

实测 · 本机执行输出
  构造器还没结束时另一个线程读到:fin = 0,plain = 0(this 逃逸:final 字段也是默认值 0)
  构造完成之后:fin = 42,plain = 42

反射与 final

实测 · 本机执行输出
  编译期常量 final int inst = 1:setAccessible(true) 之后 f.setInt(b, 99) 成功,反射读 = 99,直接 b.inst = 1(javac 把常量 1 内联进了读取处,反射改的值读不到)
  构造器里赋值的 final int computed = 5:f.setInt(b, 99) 成功,反射读 = 99,直接 b.computed = 99,b.readComputed() = 99
  热循环(200 万次,已 JIT)里读 computed 求和 = 198000000(每次都读到 99 应是 198000000;读到旧值 5 则是 10000000)
  static final 字段 STATIC:f.setInt 抛 IllegalAccessException: Can not set static final int field Main$Box.STATIC to (int)5
  record 的字段 v:f.setInt 抛 IllegalAccessException: Can not set final int field Main$Rec.v to (int)5

数据竞争发布

实测 · 本机执行输出
  [a、b 都是 final] 状态 OK;(-1, -1) 50,298,295  (0, 0) 0  (0, 1) 0  (1, 0) 0  (1, 1) 612,956
  [a 是 final,b 是普通字段] 状态 OK;(-1, -1) 97,033,103  (0, 0) 0  (0, 1) 0  (1, 0) 0  (1, 1) 30,534,788
  [a、b 都是普通字段] 状态 OK;(-1, -1) 45,508,797  (0, 0) 0  (0, 1) 0  (1, 0) 0  (1, 1) 599,894

分步演示

final 字段
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,jcstress 0.16,Apple Silicon
    • 没有验证:x86 与其它架构上普通字段是否会读到默认值、final 字段冻结(freeze)的实现细节、数组元素与 final 引用对象内部字段的可见性、JDK 17 与 27 的反射限制差异、Unsafe 修改 final 字段;jcstress 部分只验证了 default 模式 116 秒
    京ICP备2026067502号-1京公网安备11010502063487号