TL;DR
- 场景:在高并发 Java 服务中使用 Guava Cache 做本地缓存,同时需要控制刷新时延与内存占用。
- 结论:合理设置 concurrencyLevel + refreshAfterWrite,并理解 LoadingCache 的单 key 加锁语义,可在保证线程安全的前提下拿到接近 ConcurrentHashMap 的并发性能。
- 产出:给出并发参数选型思路、refresh 阻塞行为理解框架,以及对比自定义 LinkedHashMap LRU 的实现边界,方便在项目中直接落地配置。

版本矩阵
| 组件 / 能力 | 版本 / 范围 | 已验证 | 说明 |
|---|---|---|---|
| JDK 8 | 1.8.x | 是 | 典型存量项目环境,Guava Cache 并发与 refreshAfterWrite 行为一致 |
| JDK 11 | 11.x | 是 | 服务器端主流 LTS,示例代码与语义无差异 |
| JDK 17 | 17.x | 是 | 新项目常用 LTS,适用于文中所有配置与示例 |
| Guava Cache 核心 API | 23.x--32.x+ | 是 | CacheBuilder、LoadingCache、concurrencyLevel、refreshAfterWrite 语义稳定 |
| 并发分段实现(Segment/Striped) | 与 JDK ConcurrentHashMap 思路对齐 | 是 | 通过 segmentFor(hash) 进行分段定位,减少锁竞争 |
| 自定义 LRU(LinkedHashMap) | JDK 标准库 | 是 | 基于 removeEldestEntry 的访问顺序 LRU,适合单线程或外层自行加锁场景 |
Guava Cache
并发设置
Guava Cache 通过设置 concurrencyLevel 参数来优化并发性能,使得缓存能够高效地支持多线程环境下的并发读写操作。以下是关于该机制的详细说明:
-
并发级别参数详解:
- concurrencyLevel 指定了缓存内部使用的分段锁数量(默认值为4)
- 每个分段独立管理一部分缓存条目,不同分段可以并发操作
- 合理设置该值可以显著减少线程竞争(建议设置为预估并发线程数的1.5倍)
-
底层实现原理:
- 采用分段锁(Striped Lock)技术实现
- 将整个缓存划分为多个Segment(数量等于concurrencyLevel)
- 每个Segment维护自己的哈希表和读写锁
- 通过key的hashcode确定所属Segment
-
性能优化建议:
- 低并发场景(<4线程):使用默认值即可
- 中等并发(4-16线程):建议设置为8-16
- 高并发场景(>16线程):需要根据实际压力测试调整
- 设置过高会导致内存浪费,设置过低会造成锁竞争
-
实际应用示例:
java
Cache<String, Object> cache = CacheBuilder.newBuilder()
.concurrencyLevel(8) // 设置为8个分段
.maximumSize(1000)
.build();
- 注意事项:
- 该参数只在缓存构建时生效,创建后不可修改
- 与maximumSize配合使用时,每个Segment会平均分配容量限制
- 在极高并发场景下,可考虑结合refreshAfterWrite使用
这种设计使得Guava Cache在保持线程安全的同时,能够获得接近并发哈希表的性能表现,特别适合作为高性能应用中的本地缓存解决方案。
java
LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(3)
// 根据CPU情况进行并发
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
.build(new CacheLoader<String, Object>() {
@Override
public String load(String key) throws Exception {
return "get: " + key;
}
});
concurrencyLevel = Segment 数组的长度,同 ConcurrentHashMap 类似 Guava Cache 的并发也是通过分离锁实现的:
java
@CanIgnoreReturnValue // TODO(b/27479612): consider removing this
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
int hash = hash(checkNotNull(key));
return segmentFor(hash).get(key, hash, loader);
}
LoadingCache 采用了类似 ConcurrentHashMap 的方式,将映射表分多个 Segment,Segment 之间可以并发访问,这样可以大大的提高并发效率,使得并发冲突的可能性降低了。
更新锁定
Guava Cache 提供了一个 refreshAfterWrite 定时刷新数据的配置项,这个特性主要用于保证缓存数据的时效性。当配置了 refreshAfterWrite 后,如果缓存项在指定时间内没有被更新或覆盖,则会在下一次获取该值的时候触发刷新机制。
刷新过程的具体实现如下:
- 后台会启动一个异步线程去回源(如数据库、远程接口等)获取最新数据
- 在刷新期间,所有对该缓存项的请求会被阻塞(block),默认阻塞时间为 1 分钟
- 刷新过程中只有一个请求会实际执行回源操作,避免了并发回源导致的系统压力
- 如果在阻塞时间内成功获取到新值,则返回新值并更新缓存
- 如果超过阻塞时间仍未获取到新值,则会返回旧值,保证系统不会因为刷新失败而不可用
典型应用场景包括:
- 配置信息缓存(如系统参数、开关配置等)
- 商品信息缓存(如价格、库存等)
- 热点数据缓存(如排行榜数据)
示例配置代码:
java
CacheBuilder.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES) // 5分钟未更新则触发刷新
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
return fetchDataFromDB(key); // 数据加载逻辑
}
});
注意事项:
- 与 expireAfterWrite 不同,refreshAfterWrite 不会自动移除过期数据
- 建议设置合理的 refresh 时间,避免过于频繁的回源操作
- 阻塞时间可以通过重载 CacheLoader 的 reload 方法来自定义
java
LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(3)
// 根据 CPU 数量并发
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
// 3秒内阻塞的话会返回旧的数据
.refreshAfterWrite(3, TimeUnit.SECONDS)
.build(new CacheLoader<String, Object>() {
@Override
public String load(String key) throws Exception {
return "get: " + key;
}
});
动态加载
动态加载行为是缓存系统中常见的机制,它通常发生在以下两种场景:
- 首次获取数据时缓存中不存在该数据
- 缓存中的数据已经过期(基于时间或大小的过期策略)
Guava Cache 采用了一种优雅的回调模式来实现动态加载。具体实现机制如下:
-
回调模式设计:
- 用户需要预先定义数据加载方式(Loader)
- 当缓存需要加载新数据时,会自动回调这个预定义的加载方式
- 这种设计遵循了"好莱坞原则"(不要调用我们,我们会调用你)
-
线程安全处理:
- 当多个线程同时请求同一个缺失的key时
- Guava Cache 会确保只有一个线程执行加载操作
- 其他线程会等待加载完成并共享结果
-
代码实现示例:
java
// 获取对应哈希段的Segment对象
Segment<K, V> segment = segmentFor(hash);
// 调用get方法,传入key、hash值和Loader
V value = segment.get(key, hash, loader);
其中关键组件说明:
loader:用户自定义的数据加载逻辑,需要实现CacheLoader接口segmentFor(hash):Guava Cache的分段锁实现,用于提高并发性能get()方法内部会先检查缓存,若不存在则调用loader加载数据
典型应用场景:
- 数据库查询缓存
- 耗时计算结果的缓存
- 远程服务调用结果的缓存
这种设计既保证了线程安全,又提供了良好的扩展性,让使用者可以专注于业务逻辑的实现。
自定义LRU
java
package icu.wzk;
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapCache<K, V> {
private int limit;
private LRUCache<K, V> internalCache;
public LinkedHashMapCache(int limit) {
this.limit = limit;
this.internalCache = new LRUCache<>(limit);
}
public V get(K key) {
return internalCache.get(key);
}
public void put(K key, V value) {
this.internalCache.put(key, value);
}
public static void main(String[] args) {
LinkedHashMapCache<String, String> cache = new LinkedHashMapCache<>(3);
// 放入三个数据
cache.put("1", "1");
cache.put("2", "2");
cache.put("3", "3");
// 第四个数据
cache.put("4", "4");
for (Object o : cache.internalCache.values()) {
System.out.println(o);
}
}
}
class LRUCache<K,V> extends LinkedHashMap<K, V> {
private final int limit;
public LRUCache(int limit) {
super(limit, 0.75f, true);
this.limit = limit;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 移除老的数据
return size() > limit;
}
}
我们尝试运行,结果如下所示:

错误速查
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
| 并发线程数上来后,缓存命中变差、Full GC 频繁 | concurrencyLevel 设置过大,Segment 数量过多导致元数据与锁开销放大 | 检查 CacheBuilder.newBuilder() 中并发参数与实际 QPS、线程数 | 将 concurrencyLevel 控制在预估并发线程数附近,避免盲目按 CPU×N 放大 |
| 以为配置了 refreshAfterWrite 后,过期数据会自动清除 | 混淆 refreshAfterWrite(刷新)与 expireAfterWrite(过期剔除) | 通过日志或监控看到 key 长时间存在但有后台回源请求 | 需要同时根据业务配置 expireAfterWrite 或主动 invalidate |
| 高峰期 read 被卡住,线程堆栈停在 CacheLoader.load 附近 | load / 回源逻辑过慢,且 refreshAfterWrite 单 key 刷新会阻塞其他请求 | 打线程 dump,观察大量线程阻塞在同一 key 的加载路径 | 优化回源逻辑、增加超时与降级,必要时拆 key 或引入多级缓存 |
| 刷新期间期待"后台异步不阻塞",实际请求却被挂起 | 误解 refresh 语义:单 key 刷新期间,其他请求默认等待结果 | 结合文档与代码调试发现 refresh 期间返回时间抖动 | 根据业务接受度决定:改用 expire+懒加载,或对热点 key 单独设计缓存 |
| 多线程场景下自定义 LinkedHashMapCache 偶现数据错乱或 NPE | LinkedHashMap 非线程安全,外层未加锁就直接在并发环境下复用 | 在压测或线上日志中发现 size 与实际访问不一致、偶发异常 | 若需要并发,外层用 Collections.synchronizedMap 或 ReentrantLock 包裹,或直接改用 Guava Cache |
| maximumSize=3 等非常小,QPS 略高即频繁触发淘汰,命中率极差 | LRU 容量过小,未结合实际 key 数和访问分布评估 cache size | 监控中 eviction 数量远高于命中数 | 根据热 key 数量与访问模式重新估算 maximumSize,适当上调缓存容量 |
| 使用 Runtime.getRuntime().availableProcessors() 直接套到并发 | CPU 核心数≠真实并发访问线程数,导致过高或过低的锁分段配置 | 对比线程池大小、Tomcat 连接数与 CPU 核心数,发现不匹配 | 以"高并发线程数"为主维度评估 concurrencyLevel,而不是机械等于 CPU 数 |
其他系列
🚀 AI篇持续更新中(长期更新)
AI炼丹日志-29 - 字节跳动 DeerFlow 深度研究框斜体样式架 私有部署 测试上手 架构研究 ,持续打造实用AI工具指南!
AI研究-132 Java 生态前沿 2025:Spring、Quarkus、GraalVM、CRaC 与云原生落地
🔗 AI模块直达链接
💻 Java篇持续更新中(长期更新)
Java-180 Java 接入 FastDFS:自编译客户端与 Maven/Spring Boot 实战
MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务已完结,Dubbo已完结,MySQL已完结,MongoDB已完结,Neo4j已完结,FastDFS 已完结,OSS正在更新... 深入浅出助你打牢基础!
🔗 Java模块直达链接
📊 大数据板块已完成多项干货更新(300篇):
包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!
大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解
🔗 大数据模块直达链接