如何线程安全地更新Guava LoadingCache中存储的Map对应字段值
解决方案
核心需要完成两处改造保证线程安全:
- 替换缓存中存储的
HashMap为线程安全的ConcurrentHashMap,同时修正CacheLoader的load方法返回可变的线程安全Map,避免不可变集合修改报错 - 使用Guava Cache的
asMap().compute()原子方法执行更新操作,避免「先get再put」的竞态条件
改造后完整代码
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import javax.annotation.Nonnull; import java.util.concurrent.ConcurrentHashMap; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public class TestCache { private LoadingCache<String, Map<String, String>> attemptsCache; public TestCache() { attemptsCache = CacheBuilder.newBuilder() .maximumSize(10000) .expireAfterWrite(1, TimeUnit.HOURS) .build(new CacheLoader<String, Map<String, String>>() { @Override public Map<String, String> load(@Nonnull final String key) { // 返回线程安全的可变Map,替换原逻辑里不可变的Map.of() return new ConcurrentHashMap<>(); } }); } public void addCache(final String pathKey, final String inputKey, final String nameValue) throws ExecutionException { // 原初始写入逻辑可保留,也可以直接合并到下方compute逻辑实现首次写入+更新一体化 Map<String, String> initMap = new ConcurrentHashMap<>(); initMap.put("a", "1111"); attemptsCache.put(pathKey, initMap); // 线程安全更新核心逻辑 attemptsCache.asMap().compute(pathKey, (key, existingMap) -> { // 兜底兼容缓存key被过期淘汰的极端场景 if (existingMap == null) { existingMap = new ConcurrentHashMap<>(); } existingMap.put(inputKey, nameValue); return existingMap; }); } public static void main(String[] args) throws ExecutionException { new TestCache().addCache("b","a","2222"); } }
实现逻辑说明
- 线程安全Map选型
普通
HashMap不是线程安全实现,多线程并发修改时可能抛出ConcurrentModificationException,甚至破坏哈希表结构导致数据异常。ConcurrentHashMap采用CAS+细粒度锁实现并发安全,性能远高于Hashtable,完全适配缓存高频读写场景。
- 原子更新操作
直接调用
attemptsCache.get(pathKey)读取Map后修改再存入的逻辑存在竞态:如果多个线程同时操作同一个缓存key,可能出现后写入的修改被覆盖的问题。LoadingCache的asMap()视图提供的compute方法是原子操作,同一时间只会有一个线程执行同一个key的计算逻辑,保证更新的原子性。
内容的提问来源于stack exchange,提问作者Steve Jobs
相关产品推荐
相关产品推荐

