
本文介绍如何在多线程环境下安全地实现“首次调用触发异步初始化,后续调用复用同一 Future”的语义,避免重复执行耗时的 buildResult() 逻辑。核心在于用 volatile + 双重检查锁(DCL)替代方法级 synchronized。
本文介绍如何在多线程环境下安全地实现“首次调用触发异步初始化,后续调用复用同一 future”的语义,避免重复执行耗时的 `buildresult()` 逻辑。核心在于用 volatile + 双重检查锁(dcl)替代方法级 synchronized。
在异步编程中,synchronized 方法看似能保护临界区,但一旦方法返回 Future,锁即释放——而此时 Future 的实际计算可能尚未开始。正如你所观察到的:两个线程同时进入 getOrCreateResult(),第一个线程返回未完成的 ListenableFuture 后立即释放锁,第二个线程随即进入并再次触发 createResult(),最终导致 buildResult() 被并发执行多次,违背了“创建一次、共享结果”的设计初衷。
根本问题在于:同步范围与业务语义不匹配。我们真正需要同步的是“结果 Future 的创建动作”,而非整个方法调用过程;且该 Future 应被所有线程共享和复用。
✅ 推荐解决方案:volatile + 双重检查锁(Double-Checked Locking)
private volatile ListenableFuture<result> resultFuture;
public ListenableFuture<result> getOrCreateResult() {
// 第一次检查(无锁,快速命中)
if (resultFuture != null) {
return resultFuture;
}
// 加锁后二次检查(防止竞态)
synchronized (this) {
if (resultFuture == null) {
resultFuture = createResult(); // 真正的异步构建逻辑
}
}
return resultFuture;
}</result></result>
⚠️ 关键要点说明:
-
volatile修饰resultFuture是必需的:它保证了对引用的写入对其他线程立即可见,并禁止 JVM 和 CPU 对其读写重排序,这是 DCL 正确性的基石; - 两次
null检查缺一不可:第一次避免无谓加锁,第二次防止多个线程在等待锁时同时通过第一层检查并重复初始化; -
createResult()仅被调用一次,其返回的ListenableFuture<result></result>将被所有后续调用者复用——无论该 Future 是否已完成、是否失败,都遵循“创建即共享”原则。
? 进阶建议(可选增强):
- 若需支持失败后重试,可将
resultFuture封装为AtomicReference<listenablefuture>></listenablefuture>,并在捕获异常后置为null; - 使用
Suppliers.memoize()(Guava)或Lazy<t></t>(Vavr)等成熟工具类可进一步简化代码,但底层原理仍基于 DCL + volatile; - 避免在
createResult()内部再做同步——异步链本身已是线程安全的,重复同步反而降低吞吐。
总结:不要用 synchronized 方法包装返回 Future 的工厂方法;而应将 Future 实例作为共享状态,用 volatile + DCL 保障其初始化的原子性与可见性。这是构建高效、线程安全异步单例的标准实践。










