如何同步返回ListenableFuture的方法?确保buildResult仅执行一次
解决buildResult()重复执行的并发问题
你当前的问题核心是锁只保护了Future的创建流程,但没缓存正在计算的Future——第一个线程返回Future后锁就释放了,第二个线程进来时结果还没生成,所以会再次触发createResult(),导致buildResult()执行两次。
下面给出两种可靠的解决方案:
方案一:手动缓存Future(双重检查锁)
核心思路是提前缓存正在计算的ListenableFuture,而不是等结果生成后才缓存,这样所有线程都会复用同一个Future,自然buildResult()只会执行一次。
步骤1:添加Future缓存变量
在类中新增一个volatile修饰的成员变量,保证多线程下的可见性:
private volatile ListenableFuture<Result> cachedResultFuture;
步骤2:修改getOrCreateResult()方法
用双重检查锁机制,确保只有第一个线程会创建Future,后续线程直接复用缓存的Future:
public ListenableFuture<Result> getOrCreateResult() { // 非同步检查,减少锁竞争 ListenableFuture<Result> future = cachedResultFuture; if (future != null) { return future; } synchronized (this) { // 同步块内再次检查,避免并发创建 future = cachedResultFuture; if (future == null) { future = createResult(); cachedResultFuture = future; } } return future; }
步骤3:确保结果持久化
在createResult()的transform阶段,完成buildResult()后要把结果存入getResult()能读取到的存储(比如成员变量、数据库等),保证后续调用getResult()能拿到已生成的结果:
private ListenableFuture<Result> createResult() { return FluentFutures.from(this::futureComputation1) .transformAsync(this::futureComputation2, executor) .transformAsync(this::futureComputation3, executor) // ... 其他异步计算步骤 .transform(someComputation -> { Result result = buildResult(); // 将结果存入getResult()对应的存储结构 saveResult(result); return result; }, executor); }
方案二:用Guava LoadingCache简化实现
如果项目中已经引入Guava,可以直接用LoadingCache,它内置了并发控制,自动保证load方法只执行一次:
步骤1:初始化LoadingCache
private final LoadingCache<Void, Result> resultCache = CacheBuilder.newBuilder() // 可根据需求配置缓存过期时间、最大容量等参数 .build(new CacheLoader<Void, Result>() { @Override public Result load(Void key) throws Exception { // 执行异步计算并阻塞获取结果 return createResult().get(); } });
步骤2:实现getOrCreateResult()
如果需要返回ListenableFuture,使用Guava 19+提供的getAsync方法:
public ListenableFuture<Result> getOrCreateResult() { return resultCache.getAsync(null); }
如果不需要返回Future,直接用getUnchecked获取结果即可:
public Result getOrCreateResult() { return resultCache.getUnchecked(null); }
方案有效性说明
- 方案一通过缓存Future本身,让所有线程共享同一个正在计算的任务,即使第一个线程还没完成计算,后续线程也只是等待同一个Future的结果,不会重复触发
buildResult()。 - 方案二的
LoadingCache内部已经实现了成熟的并发控制逻辑,省去了手动处理双重检查锁的细节,代码更简洁易维护。
内容的提问来源于stack exchange,提问作者kane
相关产品推荐
相关产品推荐

