值对键有强引用场景下的Java缓存实现方案问询
问题背景
原Some类代码
class Some { static class Nullable extends Some { Nullable(Some wrapped) { this.wrapped = wrapped; } @Override public void doSome() {} final Some wrapped; } Some nullable() { return new Nullable(this); } public void doSome() {} }
注:原代码中nullable()方法缺少return语句,已修正。
需要为Some实例及其nullable()方法返回值实现缓存。由于缓存值(Nullable实例)持有原Some实例的强引用,是否不能使用WeakHashMap?如何在不修改Some类、不使用第三方库的前提下实现缓存?以下尝试的实现是否可行?
尝试的缓存实现代码
private static final Map<Some, Some> MAP = new HashMap<>(); // TODO: synchronize! private static final ReferenceQueue<Some> QUEUE = new ReferenceQueue<>(); private static volatile Thread thread = null; @SuppressWarnings({"unchecked"}) static Some get(final Some some) { synchronized (TheClass.class) { if (thread == null) { thread = new Thread(() -> { while (true) { try { Reference<? extends Some> reference = QUEUE.remove(); Some key = reference.get(); // can be null? Some value = MAP.remove(key); } catch (final InterruptedException ie) { Thread.currentThread().interrupt(); } } }); thread.setDaemon(true); thread.start(); } } return (Some) MAP.computeIfAbsent(some, k -> { final boolean enqueued = new WeakReference<>(k, QUEUE).enqueue(); assert enqueued; return k.nullable(); }); }
注:原代码中返回类型(T)为错误泛型,已修正为(Some)。
后续问题
reference.get()是否真的会返回null?
解答
关于WeakHashMap的疑问
没错,不能直接使用WeakHashMap。WeakHashMap的弱引用仅作用于键,但这里缓存值(Nullable实例)持有原Some实例的强引用,形成了缓存值 → 原Some实例 → WeakHashMap键的强引用链,导致原Some实例永远不会被GC回收,WeakHashMap的键也不会被自动移除,最终会造成缓存无限膨胀、内存泄漏。
尝试的实现是否可行?
思路方向正确,但存在几个致命问题:
- 线程安全缺失:
HashMap不是线程安全容器,仅同步线程初始化部分无法覆盖缓存读写的并发场景,computeIfAbsent在多线程下会出现竞态条件,导致重复创建Nullable实例或缓存数据不一致。 - WeakReference无效:在
computeIfAbsent中新建的WeakReference没有被持久化存储,会很快被GC回收,无法跟踪原Some实例的生命周期,ReferenceQueue的清理逻辑完全失效。 - 清理逻辑错误:当从ReferenceQueue取出引用时,
reference.get()确实可能返回null(原对象已被GC回收),此时用null作为键调用MAP.remove(key)无法清除缓存中的对应条目,因为缓存键是原Some实例的强引用,这会导致缓存条目永远无法被清理,引发内存泄漏。
正确的缓存实现方案
可以通过HashMap<WeakReference<Some>, Some>配合ReferenceQueue实现,核心是用WeakReference作为缓存键,跟踪原Some实例的生命周期:
private static final Map<WeakReference<Some>, Some> CACHE = Collections.synchronizedMap(new HashMap<>()); private static final ReferenceQueue<Some> REF_QUEUE = new ReferenceQueue<>(); private static volatile Thread cleanerThread; // 初始化清理线程 static { cleanerThread = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { try { // 取出被GC回收的WeakReference WeakReference<Some> ref = (WeakReference<Some>) REF_QUEUE.remove(); // 移除对应缓存条目 CACHE.remove(ref); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }); cleanerThread.setDaemon(true); cleanerThread.start(); } public static Some getNullable(Some some) { // 遍历缓存查找匹配的原实例 for (Map.Entry<WeakReference<Some>, Some> entry : CACHE.entrySet()) { Some cachedKey = entry.getKey().get(); if (cachedKey != null && cachedKey == some) { return entry.getValue(); } } // 缓存不存在则创建并放入 Some nullable = some.nullable(); WeakReference<Some> keyRef = new WeakReference<>(some, REF_QUEUE); CACHE.put(keyRef, nullable); return nullable; }
该方案的优势:
- 用
WeakReference<Some>作为缓存键,原Some实例无其他强引用时,WeakReference会被GC回收并进入ReferenceQueue - 后台清理线程监听ReferenceQueue,及时移除失效的缓存条目,避免内存泄漏
- 用
Collections.synchronizedMap保证缓存操作的线程安全
关于reference.get()是否返回null的问题
是的,reference.get()确实会返回null。当GC准备回收原Some实例时,会先将WeakReference的referent字段置为null,再将该WeakReference放入ReferenceQueue。因此当从队列中取出这个WeakReference时,调用get()得到的就是null。这也是为什么需要用WeakReference作为缓存键,而非原对象——即使referent为null,依然可以通过WeakReference本身定位并移除缓存条目。
内容的提问来源于stack exchange,提问作者Jin Kwon

