final字段的可见性保障是“一次性但确定”的:对象正确构造后,其他线程首次读取其final字段必见构造器所赋值,无需synchronized或volatile;该保障依赖构造器末尾的storestore屏障,且以无this逃逸为前提。

final 字段的内存语义不是抽象概念,它直接决定多线程下你读到的值是不是“构造时写进去的那个”。关键不在于“能不能改”,而在于“别人能不能看到你写进去的值”——只要对象正确构造,这个值就稳稳地对所有线程可见,无需 synchronized 或 volatile。
final 字段的可见性是“一次性但确定”的
Java 内存模型(JMM)规定:只要一个线程通过 new 创建对象、且构造函数完整执行完毕(即没有 this 逃逸),那么其他任意线程一旦拿到该对象引用,首次读取其 final 字段时,必定看到构造器中赋的值。
- 这种保障只发生在“对象发布后第一次读 final 字段”这个时刻,不是持续刷新的;
- 它不依赖锁或 volatile,而是靠编译器在构造器末尾自动插入 StoreStore 屏障,确保 final 写操作不会被重排到对象引用对外可见之后;
- 比如 final int id = 123;,线程 B 看到非 null 的实例引用后,id 一定是 123,绝不会是 0。
非 final 字段完全不受这个保护
final 的可见性保障严格限定在 final 字段本身,和它同在构造器里赋值的普通字段,哪怕紧挨着 final 字段下面一行,也不享受任何特殊待遇。
- 例如构造器中先写 final String name = "Alice";,再写 int age = 30;,其他线程可能看到 name="Alice" 但 age=0;
- 这是因为普通字段没有内存屏障约束,写入可能被重排序,或因缓存未刷新而不可见;
- 想让 age 也安全发布?要么加 volatile,要么用 synchronized 块包裹读写,要么把它也改成 final。
this 逃逸会直接废掉 final 语义
final 的所有保证都有一个硬前提:对象必须“正确构造完成”才对外发布。如果在构造器里就把 this 传出去(比如注册监听器、启动线程、存入静态集合),那整个保障就失效了。
- 此时其他线程可能在构造器中途就拿到 this 引用,进而读取 final 字段——而这时 final 字段可能还没写完,或者普通字段仍是默认值;
- 典型反例:new Thread(() -> System.out.println(this.name)).start(); 放在构造器里;
- 实战中应避免在构造器中调用可被子类重写的方法、发布 this、或做任何可能让 this 被外部获取的操作。
final 引用类型只保“引用不变”,不保“对象内容可见”
声明 final List
- 线程 A 构造完对象后调用 data.add("item"),线程 B 后续调用 data.size() 可能仍为 0;
- 这不是 final 失效,而是因为 ArrayList 本身不是线程安全容器,内部字段(如 elementData、size)都不是 final;
- 若需内容安全,应选用不可变容器(如 List.copyOf(...))、并发容器(如 CopyOnWriteArrayList),或配合同步机制使用。










