优化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
相关产品推荐
相关产品推荐

