Guava Cache迭代按值查键丢失数据:并发级别异常行为排查
分析与解答
首先,你遇到的情况不是Guava Cache的Bug,而是其弱一致性迭代器的预期行为,结合你的测试场景和代码细节,具体原因和解决方案如下:
核心原因:Guava Cache的弱一致性迭代器
Guava Cache的asMap()视图提供的迭代器是弱一致性设计——这是为了保证高并发场景下的性能,避免迭代时加锁带来的开销。当你设置concurrencyLevel>1时,Guava内部会将缓存拆分为多个独立的分段(类似早期ConcurrentHashMap的分段锁设计),迭代器遍历这些分段时,不会保证能看到所有当前存在的条目,尤其是在多线程并发遍历的场景下(JMH默认会使用CPU核心数对应的线程数执行基准测试)。
对比你测试的其他Map:
- Hashtable的迭代器是**快速失败(fail-fast)**的,在无并发修改的情况下,会严格遍历所有条目;
- ConcurrentHashMap的迭代器也是弱一致性,但它的分段遍历实现更稳定,在仅遍历无修改的场景下几乎不会漏看条目。
这就是为什么只有Guava Cache在concurrencyLevel>1时出现数据“丢失”的现象——本质是迭代器没返回所有条目,而非缓存本身丢失了数据。
你的代码潜在问题
另外,你的counter变量存在线程安全隐患:
private Long counter = 0L;
它是普通的包装类型,counter++涉及拆箱、自增、装箱三个非原子操作。虽然你的setup方法是单线程执行,暂时没有问题,但如果后续有并发加载缓存的场景,会导致值的竞争混乱。建议改用AtomicLong保证线程安全:
private final AtomicLong counter = new AtomicLong(0L); private Long generateIdByString(final String mString) { long current = counter.getAndIncrement(); mHashMap.put(mString, current); concurrentHashMap.put(mString, current + 1); return current + 1; }
解决方案
如果你的业务需要按值查找键,不建议遍历缓存(O(n)复杂度且有一致性问题),更可靠高效的方案是:
- 维护反向映射:在缓存加载时,同步更新一个
ConcurrentHashMap<Long, String>(值到键的映射),这样按值查键的时间复杂度是O(1),且线程安全。 - 避免依赖asMap()遍历:如果必须遍历缓存,且需要强一致性,可以在遍历前调用
cache.cleanUp()清理过期条目,或者手动加锁,但这会牺牲并发性能,仅适合低并发场景。
总结
你遇到的是Guava Cache为了并发性能做出的设计取舍——弱一致性迭代器是预期行为,而非Bug。对于按值查找键的场景,维护反向映射是最优解。
内容的提问来源于stack exchange,提问作者agilob
相关产品推荐
相关产品推荐

