应手写带前后指针的node节点,用concurrenthashmap快速定位,reentrantlock控制链表头尾及增删操作,确保get时“移到头”原子完成,淘汰仅在put后触发并同步清理引用。

用 ConcurrentHashMap + LinkedHashMap 组合会出错
直接继承 LinkedHashMap 并加 synchronized 或用 ConcurrentHashMap 替代底层存储,看似合理,实则破坏 LRU 的访问顺序一致性。因为 ConcurrentHashMap 不保证遍历顺序,而 LinkedHashMap 的 accessOrder = true 行为在多线程下会被并发修改打断——比如两个线程同时触发 get(),一个还没完成节点移动,另一个就已开始迭代淘汰,导致 removeEldestEntry 判定失效或抛 ConcurrentModificationException。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 放弃“复用
LinkedHashMap顺序逻辑 + 外层并发控制”的思路,它本质上不可靠 - 不直接暴露
put/get方法给外部调用,避免绕过封装的同步逻辑 - 若必须基于
LinkedHashMap,只能整个 map 加锁(如synchronized (map)),但会严重拖慢读性能
ReentrantLock 配合双链表手写节点比用 ConcurrentLinkedQueue 更可控
常见误区是想用无锁队列管理访问顺序,但 ConcurrentLinkedQueue 不支持按需删除中间节点(LRU 淘汰时要删任意旧节点),也不提供 O(1) 的头/尾访问+更新能力。手写带前后指针的节点,配合 ReentrantLock 控制 head/tail 和节点增删,反而更清晰、边界明确。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 每个缓存项封装为
Node<k v></k>,含key、value、prev、next - 用一个
ConcurrentHashMap<k node>></k>快速定位节点,再用独占锁操作链表结构 - 锁粒度聚焦在链表头尾和节点摘除/插入动作上,不是整个缓存实例
- 注意:
get()中的“移到链表头”必须原子完成(先断开再拼到 head),否则可能造成节点丢失
computeIfAbsent 在初始化缓存值时不适用
如果业务逻辑里用 cache.computeIfAbsent(key, k -> heavyLoad(k)),看似简洁,但在高并发下会多次执行 heavyLoad —— 因为 computeIfAbsent 只保证 key 不存在时才调用 mapping function,但多个线程可能同时发现 key 缺失,各自触发加载。这不是缓存本身的问题,而是使用方式放大了竞争。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 对加载成本高的 key,应在外层用
ConcurrentHashMap的computeIfAbsent做“加载锁”,再把结果塞进 LRU 缓存 - LRU 缓存类自身提供的
get方法不应承担加载职责,只做纯内存查找与顺序维护 - 若非要一体化,得自己实现类似
computeIfPresentOrLoad的逻辑,内部用Future或双重检查 +putIfAbsent
容量淘汰触发点选在 put 后而非 get 中
有人把淘汰逻辑放在 get 方法末尾,想着“顺手清理”,结果导致读请求变慢、且无法控制淘汰节奏——比如突发大量冷 key 访问,每次 get 都触发一次淘汰扫描,CPU 毛刺明显。更糟的是,如果淘汰需要 I/O(如写日志)或远程通知,会彻底拖垮响应时间。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 淘汰只在
put插入新节点后检查 size,超限时循环移除 tail 节点 - 移除时同步从
ConcurrentHashMap中remove(key),防止残留引用 - 不依赖
ReferenceQueue或后台线程清理,避免 GC 延迟带来的内存不确定性 - 如果缓存项有 closeable 资源(如文件句柄),淘汰时必须显式调用
close(),不能等 GC
真正麻烦的是节点引用关系的生命周期管理:key 被 remove 后,对应 node 的 prev/next 若没置 null,GC 无法回收整条链;而提前置 null 又可能被其他线程误用。这个细节,十次实现九次漏。









