懒初始化引用且写入值恒定时,是否具备线程安全性?
首先得说,你的思路有一定合理性——毕竟对象引用的读写在Java里确实是原子操作,而且你的初始化方法是幂等的,就算多个线程重复计算,结果也完全一致。但这里有个很容易被忽略的陷阱,会直接导致代码不是线程安全的:指令重排序引发的对象半初始化问题。
为什么这么说?Java虚拟机为了优化性能,可能会对代码的执行顺序进行重排序。比如在你的代码里,线程A执行views = ListUtil.toUnmodifiableList(viewsListString);之后,JVM可能会把this.views = views;这个赋值操作提前,而此时views指向的List对象还没完成初始化(比如集合内部的元素还没完全赋值)。这时候如果线程B过来检查this.views,会发现它不是null,直接返回这个“半成品”List,后续访问的时候就可能抛出NullPointerException,或者看到不一致的集合状态——这绝对是你不想看到的。
你提到的幂等性只能解决“重复计算结果一致”的问题,但解决不了可见性和半初始化的问题。虽然重复计算不会导致逻辑错误,但半初始化的对象是致命的。
那怎么解决这个问题?给你两个常用的方案:
方案一:volatile + 双重检查锁定
这是标准的线程安全懒初始化方案,虽然看起来有点冗余,但能彻底解决指令重排序和可见性问题。修改后的代码如下:
private volatile List<String> views; private List<String> getViews(ServletContext servletContext) { List<String> localViews = this.views; // 局部变量提升性能 if (localViews == null) { synchronized (this) { localViews = this.views; if (localViews == null) { String viewsListString = servletContext.getInitParameter( "my.views.list.VIEWS_LIST"); localViews = ListUtil.toUnmodifiableList(viewsListString); this.views = localViews; } } } return localViews; }
这里的volatile关键字会禁止JVM对初始化和赋值操作进行重排序,同时保证一个线程对views的修改能立刻被其他线程看到。
方案二:静态内部类Holder模式
如果你觉得双重检查锁不够优雅,也可以利用Java类初始化的线程安全性来实现懒加载,代码更简洁:
private List<String> getViews(ServletContext servletContext) { return ViewHolder.getInitializedViews(servletContext); } // 静态内部类只会在第一次被访问时初始化,且JVM保证这个过程是线程安全的 private static class ViewHolder { private static List<String> views; private static List<String> getInitializedViews(ServletContext servletContext) { if (views == null) { synchronized (ViewHolder.class) { if (views == null) { String viewsListString = servletContext.getInitParameter( "my.views.list.VIEWS_LIST"); views = ListUtil.toUnmodifiableList(viewsListString); } } } return views; } }
这个模式的核心是,静态内部类ViewHolder的初始化只会在第一次调用getInitializedViews时触发,而JVM会保证类初始化过程中的线程安全性,不需要额外的volatile修饰(当然加了也没问题)。
总结一下:你的原始代码因为缺少volatile,无法避免指令重排序带来的半初始化风险,所以不是线程安全的。必须通过上述两种方式之一来修复,才能保证多线程环境下的安全访问。
内容的提问来源于stack exchange,提问作者stiemannkj1

