使用AtomicReference实现延迟初始化是否具备线程安全性?
AtomicReference实现延迟初始化的线程安全性验证
我们团队在讨论用AtomicReference实现延迟初始化的线程安全性时产生了争议:有同事认为首批执行的线程可能获取到null值,导致初始化失败。为此我写了如下测试代码——拆分get与compareAndSet操作是为了避免每次调用开销较高的costlyGetValue()方法,测试从未失败,想确认这个实现是否真的安全。
public class TestAtomicReference { static final AtomicReference<String> A = new AtomicReference<>(); public static void main(String[] args) throws Exception { Callable<Integer> callable = () -> { lazyInit(); return 0; }; ExecutorService executorService = Executors.newFixedThreadPool(50); for (int i = 0; i < 1000; i++) { A.set(null); List<Future<Integer>> futures = executorService.invokeAll(Collections.nCopies(50, callable)); futures.forEach(f -> { try { f.get(); } catch (Exception ignore) { } }); } executorService.shutdown(); executorService.awaitTermination(10, TimeUnit.SECONDS); } private static void lazyInit() { if (A.get() == null) { A.compareAndSet(null, costlyGetValue()); } if (A.get() == null) { System.out.println("INIT FAILURE!"); } } private static String costlyGetValue() { return "A"; } }
这个实现是线程安全的,绝不会出现初始化失败(即打印"INIT FAILURE!")的情况,原因如下:
- AtomicReference的
get()和compareAndSet()都是原子操作,且JVM保证这些操作的内存可见性:一个线程对AtomicReference的修改,其他线程能立即读取到最新值。 - 当多个线程同时进入
lazyInit():- 第一个执行
A.get()的线程会发现值为null,进而执行compareAndSet(null, costlyGetValue()),成功将A的值设置为"A"。 - 后续线程不管是在第一个线程完成CAS前还是后进入第一个
if判断,当它们执行第二个A.get()时,结果必然不为null:要么A已经被第一个线程设置完成,要么自己执行CAS时发现A已经有值(CAS失败),但此时A的最新值已经存在。
- 第一个执行
- 就算多个线程同时调用
costlyGetValue(),在当前测试里只是多生成了几个相同的字符串(字符串常量池会复用,实际额外开销极小),但最终只有一个线程能CAS成功,其他线程的CAS失败不会影响A的最终状态。
需要注意的是:如果costlyGetValue()是带有副作用的方法(比如创建独占资源、修改全局状态),那么这种实现会导致该方法被多次调用——如果不能接受这种情况,可能需要搭配同步块来控制;但如果只是单纯开销高但无副作用,这个实现完全可以放心使用。
内容的提问来源于stack exchange,提问作者PZ73
相关产品推荐
相关产品推荐

