如何实现键值互换WeakHashMap并优化竞态保护与内存可见性
实现方案与问题解答
完整优化后实现代码
@Singleton public class UUIDToResultSetMapper { // 引用队列,接收被GC回收的ResultSet对应的弱引用 private final ReferenceQueue<ResultSet> refQueue = new ReferenceQueue<>(); // 核心存储:键为UUID(强引用),值为绑定了UUID的ResultSet弱引用 private final ConcurrentHashMap<String, ResultSetWeakRef> map = new ConcurrentHashMap<>(); // 自定义弱引用子类,绑定对应UUID,用于GC后清理Map中的无效条目 private static class ResultSetWeakRef extends WeakReference<ResultSet> { private final String uuid; public ResultSetWeakRef(String uuid, ResultSet referent, ReferenceQueue<ResultSet> queue) { super(referent, queue); this.uuid = uuid; } public String getUuid() { return uuid; } } // 清理已被GC的无效条目,每次读写操作前调用即可,无额外线程开销 private void clearInvalidEntries() { ResultSetWeakRef ref; while ((ref = (ResultSetWeakRef) refQueue.poll()) != null) { map.remove(ref.getUuid()); } } public void put(String key, ResultSet value) { clearInvalidEntries(); // compute为原子操作,天然避免竞态,无需额外加锁 map.compute(key, (k, existingRef) -> { // 原逻辑:UUID已存在且对应ResultSet未被GC则抛出异常 if (existingRef != null && existingRef.get() != null) { throw new RuntimeException("UUID is still in the map"); } System.out.println("put into map: " + key); return new ResultSetWeakRef(key, value, refQueue); }); System.out.println("current map size at put:" + map.size()); } public ResultSet get(String key) { clearInvalidEntries(); System.out.println("current map size at get:" + map.size()); // 打印当前所有有效UUID map.values().forEach(ref -> { if (ref.get() != null) { System.out.println(ref.getUuid()); } }); System.out.println("search for: " + key); ResultSetWeakRef ref = map.get(key); return ref != null ? ref.get() : null; } }
对应问题解答
1. 基于WeakReference的合规实现
你原有实现是通过键值互换借用WeakHashMap键弱引用的能力,属于hack用法。合规实现逻辑如下:
- 用UUID作为Map的键,直接强引用保留
- 自定义WeakReference子类,同时持有ResultSet和对应的UUID,将ResultSet设为弱引用目标
- 搭配ReferenceQueue接收被GC回收的ResultSet对应的弱引用实例
- 每次读写操作前拉取ReferenceQueue中的无效引用,删除Map中对应的条目
该实现完全符合需求:只要ResultSet被业务逻辑持有强引用处于活跃状态,对应条目就会保留;一旦ResultSet被GC回收,对应条目会被自动清理,无需手动释放。
2. put方法防竞态优化方案
原有实现存在两个明显问题:一是为了校验UUID是否存在需要遍历全表,时间复杂度为O(n),性能极差;二是通过全局synchronized加锁,并发度低。
优化方案:
- 直接用UUID作为Map的键,存在性校验从O(n)降为O(1)
- 用ConcurrentHashMap的
compute方法完成校验+插入的原子操作,无需额外加锁,天然避免竞态,并发性能远高于全局synchronized锁。如果不需要严格的重复插入校验,还可以用性能更高的putIfAbsent方法。
3. get方法happens-before关系实现
原有实现中空的synchronized块属于非常不规范的内存屏障用法,性能损耗高且可读性差。
最优方案是直接使用ConcurrentHashMap作为存储容器,Java并发规范明确保证:ConcurrentHashMap的写入操作 happens-before 后续对同一位置的读取操作,所有变更天然对读线程可见,完全不需要手动加任何内存屏障。
如果你确实不想使用ConcurrentHashMap,也可以用Java 9+提供的VarHandle.fullFence()手动加内存屏障代替空synchronized块,但复杂度和收益完全不成正比,不推荐使用。
内容的提问来源于stack exchange,提问作者Robin Kreuzer
相关产品推荐
相关产品推荐

