Caffeine缓存正确失效与清理问题求助:多线程场景下已关闭对象仍被获取的异常排查
Caffeine缓存正确失效与清理问题求助:多线程场景下已关闭对象仍被获取的异常排查
看起来你在Caffeine缓存的多线程使用中踩了竞态条件和缓存生命周期管理的坑,这个This object is closed异常的出现,核心是检查对象状态与后续操作的非原子性,加上Caffeine异步驱逐的特性,导致了不可控的线程安全问题。咱们一步步拆解问题,再给出针对性的解决方案。
一、核心问题分析
先看你的代码逻辑里的关键竞态窗口,这是异常出现的直接原因:
var found = cache.get(value); // 【时间窗口】线程B可能在此处修改found的isClosed状态 if (found.isClosed) { cache.invalidate(value); cache.cleanUp(); found = cache.get(value); } // 此时found可能已经被线程B关闭,调用getValue()会抛出异常 if (found.getValue() != value) { throw new IllegalStateException(...); }
结合Caffeine的特性,还有两个隐藏的设计冲突:
- 异步驱逐的延迟:Caffeine默认采用异步方式进行缓存驱逐(比如达到
maximumSize后的LRU移除),对象被标记为待移除后,可能仍短暂存在于缓存中,直到异步清理完成,此时你持有的对象引用可能已经被removalListener关闭。 - 缓存对象的状态不稳定性:你将缓存对象的
isClosed状态与缓存生命周期绑定,当对象被移除出缓存时会被关闭,但业务代码可能仍持有该对象的引用,导致无效对象被误用。
二、针对性解决方案
我们需要从原子性操作和缓存生命周期与对象状态解耦两个维度修复问题,避免手动检查对象状态的竞态风险。
方案1:优先使用不可变缓存对象(推荐,无资源清理场景)
如果你的Temp对象不需要清理资源(只是示例场景),直接将其设计为不可变类,彻底避免状态修改带来的线程安全问题:
class Temp { private final int value; public Temp(int value) { this.value = value; } public int getValue() { return value; } }
同时移除removalListener,因为不可变对象无需关闭。这种方案从根源上消除了状态不一致的可能。
方案2:原子化获取有效对象(资源清理场景适用)
如果Temp持有必须清理的资源(如文件句柄、数据库连接),则需要利用Caffeine底层ConcurrentMap的原子操作,确保获取到的对象一定是有效的。
核心思路是用cache.asMap().compute替代手动的检查-失效逻辑,该方法是原子性的,能完全消除竞态窗口:
public static void main(String[] args) { LoadingCache<Integer, Temp> cache = Caffeine.newBuilder() .maximumSize(5) .removalListener((key, value, cause) -> { try { ((AutoCloseable) value).close(); } catch (Exception e) { throw new RuntimeException(e); } }) .build(Trial2::getObj); Random random = new Random(); for (int i = 0; i < 1000000; i++) { int value = random.nextInt(1000); // 原子化操作:要么返回已存在的有效对象,要么创建新对象 var found = cache.asMap().compute(value, (k, existing) -> { if (existing == null || existing.isClosed) { return getObj(k); } return existing; }); try { if (found.getValue() != value) { throw new IllegalStateException("Value mismatch: expected " + value + ", got " + found.getValue()); } } catch (IllegalStateException e) { throw new RuntimeException("Unexpected error accessing object", e); } } System.out.println("All values matched successfully!"); }
这种方式的优势:
- 原子性:
compute方法在整个操作期间会锁定对应的key,避免多线程下的竞态条件。 - 有效性保证:直接在原子操作中判断对象状态,确保返回的对象一定未被关闭。
方案3:资源池替代缓存(重度资源依赖场景)
如果Temp是需要频繁复用的重量级资源(如数据库连接池、线程池),缓存不是最佳选择,应该使用专门的资源池框架(如Apache Commons Pool2)。资源池会专门管理对象的借用、归还、销毁生命周期,比缓存更适合需要严格资源清理的场景。
三、多线程场景下的额外注意事项
- 优先使用Caffeine的原子操作:Caffeine的
LoadingCache和底层ConcurrentMap提供了get、compute、merge等原子操作,避免手动的检查-修改逻辑,减少竞态窗口。 - 谨慎使用
removalListener:removalListener在对象被移除时触发,此时可能还有其他线程持有对象引用,因此清理资源时要通过线程安全的状态标记(如你的volatile isClosed+重入锁)保证安全。 - 缓存对象尽量不可变:缓存的核心是提升读取性能,不可变对象能从根源上避免状态不一致的问题,是缓存设计的最佳实践。
内容来源于stack exchange
相关产品推荐
相关产品推荐

