Java中final修饰的List<T>与多线程环境下的线程安全问题
问题核心原因解释
1. 误用了final字段的内存语义生效前提
Java内存模型中final字段的「冻结(freeze)」语义生效的必要前提是:对象引用没有在构造函数执行过程中逸出。
你在Dummy类的构造函数中直接将this赋值给了外部静态变量_Thread.dummy,属于典型的构造过程中this逸出,直接破坏了final语义的生效条件。
JMM对final字段的保障规则是:所有对final字段的写入、以及该final字段可到达的对象的写入操作,都会在构造函数完全执行完毕、对象引用被赋值给其他变量之前完成冻结。只有其他线程是在构造函数执行完成后才拿到对象引用时,才能保证看到final字段的完整初始化结果。你在构造函数的循环填充集合逻辑之前就把半构造的对象引用发布出去,final的保障自然完全不生效。
2. NPE异常的来源
你的main线程执行顺序是先启动_Thread,再执行new Dummy()。如果_Thread的run()方法执行时,Dummy对象还没有开始构造,_Thread.dummy仍然是null,调用dummy.getIntegers()就会触发空指针异常。
3. 执行结果不可预测的原因
在没有任何同步机制的前提下,两个线程的执行顺序完全没有确定性:
- 如果
_Thread拿到非null的dummy引用时,构造函数中的for循环还没执行完,integers的size就会小于10000,输出false - 如果for循环刚好执行完
_Thread才读取size,就会输出true
这和是否加final修饰没有关系,因为你的this逸出已经让final的保障失效,所以加不加final结果都不可控。
正确测试final语义的写法
如果要验证final的线程安全保障,需要把_Thread.dummy = this;移到构造函数外部,也就是等Dummy对象完全构造完成后再发布对象引用:
// main方法修改为 public static void main(String[] args) { new _Thread().start(); // 先构造完成再发布引用 Dummy dummy = new Dummy(); _Thread.dummy = dummy; } // Dummy构造函数去掉逸出逻辑 public Dummy() { for (int a = 0; a < 10_000; a++) { integers.add(a); } }
这种情况下,只要_Thread能拿到非null的dummy引用,就一定能看到integers已经完成10000个元素的填充,不会出现size小于10000的情况。
内容的提问来源于stack exchange,提问作者zexed640
相关产品推荐
相关产品推荐

