You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

优化Caffeine的Cache.get方法:映射函数失败时让所有等待线程同步失败

解决Caffeine缓存多线程加载失败同步问题

Caffeine默认逻辑下,当多个线程请求同一个不存在的key时,第一个线程会执行映射函数,其他线程等待结果。但如果第一个线程的映射函数抛出异常,后续等待的线程会重新执行映射函数,这不符合你期望的"所有等待线程同步失败"的需求。要实现目标效果,可通过以下两种方式处理:

方式一:使用AsyncLoadingCache(推荐)

AsyncLoadingCache基于CompletableFuture实现异步加载,多个线程请求同一个key时会共享同一个CompletableFuture实例。当第一个线程的加载任务抛出异常时,所有等待该key的线程都会收到同一个异常,不会重试加载。

示例代码:

// 初始化异步加载缓存
AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
    .buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> {
        // 模拟1分钟后加载失败的逻辑
        try {
            Thread.sleep(60000);
            throw new RuntimeException("第一个加载任务失败");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("加载被中断");
        }
    }, executor));

// 三个线程并行调用
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            cache.get("targetKey").get();
        } catch (Exception e) {
            System.out.printf("线程%s加载失败:%s%n", 
                Thread.currentThread().getName(), e.getCause().getMessage());
        }
    }).start();
}

当第一个线程的加载任务抛出异常后,另外两个线程会立即捕获到相同的异常,不会重复执行加载逻辑。

方式二:自定义同步LoadingCache的加载逻辑

如果必须使用同步的LoadingCache,可以通过内部维护加载任务缓存,让多个线程共享同一个加载结果(包括异常),避免重试。

示例代码:

LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .build(new CacheLoader<String, Object>() {
        // 缓存正在进行的加载任务
        private final ConcurrentMap<String, CompletableFuture<Object>> pendingTasks = new ConcurrentHashMap<>();

        @Override
        public Object load(String key) throws Exception {
            // 确保同一个key只有一个加载任务在执行
            CompletableFuture<Object> loadTask = pendingTasks.computeIfAbsent(key, k -> 
                CompletableFuture.supplyAsync(() -> {
                    try {
                        Thread.sleep(60000);
                        throw new RuntimeException("第一个加载任务失败");
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        throw new RuntimeException("加载被中断");
                    } finally {
                        // 加载完成(无论成功失败)移除任务缓存
                        pendingTasks.remove(key);
                    }
                })
            );

            try {
                return loadTask.get();
            } catch (ExecutionException e) {
                // 解包实际异常抛出
                throw new RuntimeException(e.getCause());
            }
        }
    });

// 三个线程并行调用
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            cache.get("targetKey");
        } catch (Exception e) {
            System.out.printf("线程%s加载失败:%s%n", 
                Thread.currentThread().getName(), e.getMessage());
        }
    }).start();
}

这个方案通过ConcurrentHashMap缓存正在执行的加载任务,确保同一个key的所有请求共享同一个任务结果,当任务失败时所有等待线程都会同步收到异常。

核心原理

Caffeine默认的同步加载逻辑中,当第一个加载任务失败时,会清除内部的"加载中"标记,导致后续线程认为key不存在而重新执行加载。而上述两种方案的核心是让所有线程共享同一个加载任务的结果(包括异常状态),从而实现第一个失败时所有等待线程同步失败的效果。

内容的提问来源于stack exchange,提问作者user3676029

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 09:35:19