Java构造器操作对后续线程的可见性及代码合规性问询
现有如下Java类:
public class MyClass { private final List<String> myList = new ArrayList<>(); // 非线程安全类 // 线程1调用 MyClass() { myList.add("foo"); } // 线程2调用 void add(String data) { myList.add(data); } }
该类是否符合线程安全要求?
仅找到JLS规范中的如下描述:
对象在构造器执行完毕后被视为完全初始化。只能在对象完全初始化后看到其引用的线程,将被保证能看到该对象final字段的正确初始化值。
这是否意味着线程2一定能看到ArrayList实例,但可能看不到其中的"foo"元素?
换而言之,是否可能出现如下执行顺序:
- T1: 创建
MyClass及ArrayList实例 - T2: 访问
ArrayList实例 - T1: 向列表添加"foo"
结论:这个类不符合线程安全要求,线程2确实可能看不到构造器中添加的"foo"元素
核心原因分析
final字段的可见性保障有边界
JLS的规范仅保证线程能看到myList这个final引用指向的ArrayList实例是正确初始化的,但不覆盖该实例内部的状态。myList是final的,所以线程2肯定能拿到这个ArrayList对象,但构造器里给ArrayList添加的"foo"属于集合内部状态,不在JLS的可见性保障范围内。构造器操作可能被重排序
在无同步约束的情况下,JVM允许对构造器内的操作重排序。比如线程1中,new ArrayList<>()初始化、引用赋值给myList、myList.add("foo")这几步,可能被调整为:先创建ArrayList并赋值给myList,此时线程2若拿到MyClass的引用并访问myList,就会先于add("foo")执行,导致看不到该元素。ArrayList本身非线程安全
即便构造器操作没有重排序,线程2调用myList.add(data)与线程1构造器中的add操作属于并发修改,这本身就违反了ArrayList的线程安全约定,会引发元素丢失、结构损坏等不可预期的问题。
线程安全修复方案
可以通过以下方式实现线程安全:
- 替换为线程安全集合:使用
CopyOnWriteArrayList或Collections.synchronizedList(new ArrayList<>())替代原生ArrayList - 添加同步机制:给
add方法和构造器中对myList的操作加锁,确保所有操作串行执行 - 安全发布对象:通过
volatile变量发布MyClass实例,或在同步块中完成实例发布,同时配合集合操作的同步。
内容的提问来源于stack exchange,提问作者user996142

