Java并发:使用WeakReference实现并发容器中的Value失效机制
大家好,今天我们来深入探讨一个在并发编程中非常有用的技巧:如何利用 WeakReference 实现并发容器中Value的失效机制。在某些场景下,我们希望容器中的Value对象在不再被其他地方引用时,能够自动从容器中移除,从而释放内存资源。WeakReference 就能很好地帮助我们实现这个目标。
1. 为什么需要Value失效机制?
在并发环境中,我们经常使用容器来缓存一些计算结果或者对象,以便提高性能。例如,我们可以使用 ConcurrentHashMap 来缓存用户的会话信息,或者缓存一些昂贵的计算结果。但是,如果这些缓存的Value对象不再被其他地方引用,它们就会一直占用内存空间,导致内存泄漏。
Value失效机制的引入是为了解决这个问题。通过设置Value的失效策略,我们可以让容器在Value对象不再被引用时,自动将其从容器中移除,从而释放内存资源。
2. WeakReference 简介
java.lang.ref.WeakReference 是Java提供的一种弱引用类型。与强引用不同,弱引用不会阻止垃圾回收器回收被引用的对象。也就是说,如果一个对象只被弱引用所引用,那么当垃圾回收器运行时,这个对象就会被回收。
WeakReference 的主要特点如下:
- 不阻止垃圾回收: 这是最重要的特性。垃圾回收器在回收对象时,会忽略弱引用。
- 可以判断对象是否已被回收: 通过
WeakReference.get()方法可以获取引用的对象。如果对象已被回收,该方法返回null。
3. 使用 WeakReference 实现失效机制的基本思路
实现失效机制的基本思路是:
- 将Value对象包装成
WeakReference。 - 将
WeakReference存储在并发容器中。 - 定期检查容器中的
WeakReference,如果WeakReference.get()返回null,则说明Value对象已被回收,从容器中移除对应的Key-Value对。
4. 具体实现:WeakValueMap
下面我们来实现一个简单的 WeakValueMap,它使用 WeakReference 来存储Value,并定期检查失效的Value。
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class WeakValueMap<K, V> {
private final ConcurrentMap<K, WeakReference<V>> map = new ConcurrentHashMap<>();
private final ReferenceQueue<V> queue = new ReferenceQueue<>();
public V get(K key) {
expungeStaleEntries(); // 清理失效的entry
WeakReference<V> weakReference = map.get(key);
if (weakReference != null) {
return weakReference.get(); // 返回value,如果已经被回收,则返回null
}
return null;
}
public V put(K key, V value) {
expungeStaleEntries(); // 清理失效的entry
WeakReference<V> weakReference = new WeakReference<>(value, queue);
WeakReference<V> previous = map.put(key, weakReference);
if (previous != null) {
return previous.get();
}
return null;
}
public V remove(K key) {
expungeStaleEntries(); // 清理失效的entry
WeakReference<V> weakReference = map.remove(key);
if (weakReference != null) {
return weakReference.get();
}
return null;
}
// 清理已经被回收的value对应的entry
@SuppressWarnings("unchecked")
private void expungeStaleEntries() {
Reference<? extends V> ref;
while ((ref = queue.poll()) != null) {
for (K key : map.keySet()) {
if (map.get(key) == ref) {
map.remove(key);
break;
}
}
}
}
public int size() {
expungeStaleEntries();
return map.size();
}
public boolean containsKey(K key) {
expungeStaleEntries();
return map.containsKey(key);
}
public void clear() {
map.clear();
// 清空queue,防止内存泄漏
while (queue.poll() != null) {}
}
public static void main(String[] args) throws InterruptedException {
WeakValueMap<String, Object> map = new WeakValueMap<>();
Object obj1 = new Object();
Object obj2 = new Object();
map.put("key1", obj1);
map.put("key2", obj2);
System.out.println("Map size before GC: " + map.size()); // Expected: 2
obj1 = null; // 解除obj1的强引用
System.gc(); // 触发垃圾回收
Thread.sleep(100); // 等待垃圾回收完成
System.out.println("Map size after GC: " + map.size()); // Expected: 1 (key1被移除)
System.out.println("Value of key1 after GC: " + map.get("key1")); // Expected: null
System.out.println("Value of key2 after GC: " + map.get("key2")); // Expected: not null
obj2 = null;
System.gc();
Thread.sleep(100);
System.out.println("Map size after GC: " + map.size()); // Expected: 0 (key2被移除)
map.put("key3", new Object());
System.out.println("Map size before clear: " + map.size());
map.clear();
System.out.println("Map size after clear: " + map.size());
}
}
代码解释:
map: 使用ConcurrentHashMap存储 Key 和WeakReference<V>的映射。queue:ReferenceQueue用于跟踪被垃圾回收器回收的Value对象。当一个Value对象被回收时,对应的WeakReference会被放入这个队列中。get(K key): 获取指定Key对应的Value。首先调用expungeStaleEntries()清理失效的entry,然后从map中获取WeakReference,如果WeakReference.get()返回null,则说明Value对象已被回收,返回null。put(K key, V value): 将Key-Value对放入map中。首先调用expungeStaleEntries()清理失效的entry,然后将Value对象包装成WeakReference,并放入map中。同时将queue传递给WeakReference的构造函数,以便跟踪Value对象的回收情况。remove(K key): 从map中移除指定Key对应的Key-Value对。首先调用expungeStaleEntries()清理失效的entry,然后从map中移除 Key-Value对。expungeStaleEntries(): 清理失效的entry。该方法从queue中获取被回收的WeakReference,然后遍历map,找到对应的Key,并从map中移除该Key-Value对。clear(): 清空map和queue,防止内存泄漏
5. 关键点和注意事项
ReferenceQueue的使用:ReferenceQueue是实现失效机制的关键。当垃圾回收器回收一个被WeakReference引用的对象时,会将该WeakReference放入ReferenceQueue中。我们可以通过检查ReferenceQueue来判断哪些Value对象已被回收。- 定期清理: 需要定期调用
expungeStaleEntries()方法来清理失效的entry。可以在get(),put(),remove()等方法中调用,也可以使用一个定时任务来定期清理。 - 并发安全:
ConcurrentHashMap保证了并发安全。 - 内存泄漏: 如果在移除
WeakReference后没有及时清理ReferenceQueue,可能会导致内存泄漏。所以clear()方法中需要清空queue。 - 性能考量: 频繁的清理操作可能会影响性能。需要根据实际情况调整清理的频率。
- Key的强引用: 虽然Value是弱引用,但是Key仍然是强引用。如果Key不再被使用,也需要手动移除。
6. 扩展:使用 Cleaner 替代 ReferenceQueue (JDK 9+)
在JDK 9及以上版本中,可以使用 java.lang.ref.Cleaner 替代 ReferenceQueue 来实现更方便的资源清理。Cleaner 提供了一种更简洁的机制来注册对象清理操作。
import java.lang.ref.Cleaner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class WeakValueMapWithCleaner<K, V> {
private final ConcurrentMap<K, V> map = new ConcurrentHashMap<>();
private final Cleaner cleaner = Cleaner.create();
private static class State<K, V> implements Runnable {
private final K key;
private final ConcurrentMap<K, V> map;
public State(K key, ConcurrentMap<K, V> map) {
this.key = key;
this.map = map;
}
@Override
public void run() {
map.remove(key);
}
}
public V get(K key) {
return map.get(key);
}
public V put(K key, V value) {
V previous = map.put(key, value);
cleaner.register(value, new State<>(key, map));
return previous;
}
public V remove(K key) {
return map.remove(key);
}
public int size() {
return map.size();
}
public boolean containsKey(K key) {
return map.containsKey(key);
}
public void clear() {
map.clear();
}
public static void main(String[] args) throws InterruptedException {
WeakValueMapWithCleaner<String, Object> map = new WeakValueMapWithCleaner<>();
Object obj1 = new Object();
Object obj2 = new Object();
map.put("key1", obj1);
map.put("key2", obj2);
System.out.println("Map size before GC: " + map.size()); // Expected: 2
obj1 = null; // 解除obj1的强引用
System.gc(); // 触发垃圾回收
Thread.sleep(100); // 等待垃圾回收完成
System.out.println("Map size after GC: " + map.size()); // Expected: 1 or 2 (取决于GC执行情况)
System.out.println("Value of key1 after GC: " + map.get("key1")); // Expected: null (可能)
System.out.println("Value of key2 after GC: " + map.get("key2")); // Expected: not null
obj2 = null;
System.gc();
Thread.sleep(100);
System.out.println("Map size after GC: " + map.size()); // Expected: 0, 1 or 2 (取决于GC执行情况)
}
}
代码解释:
cleaner: 创建一个Cleaner实例。State: 一个实现了Runnable接口的内部类,用于执行清理操作。当Value对象被回收时,Cleaner会调用State.run()方法,该方法会从map中移除对应的Key-Value对。put(K key, V value): 将Key-Value对放入map中,并使用cleaner.register()方法注册清理操作。
使用 Cleaner 的优点:
- 更简洁: 无需手动处理
ReferenceQueue。 - 更安全:
Cleaner保证清理操作最终会被执行。
7. 应用场景
- 缓存: 缓存计算结果或对象,避免重复计算。
- 会话管理: 存储用户会话信息。
- 对象池: 管理可重用的对象。
- 数据加载: 缓存从数据库或网络加载的数据。
8. 总结
今天我们学习了如何使用 WeakReference 实现并发容器中Value的失效机制。通过将Value对象包装成 WeakReference,我们可以让容器在Value对象不再被引用时,自动将其从容器中移除,从而释放内存资源。我们还介绍了如何使用 ReferenceQueue 和 Cleaner 来跟踪Value对象的回收情况,并实现了 WeakValueMap 和 WeakValueMapWithCleaner 两个示例。
9. 进一步思考
- 如何选择合适的失效策略?
- 如何监控容器的内存使用情况?
- 如何与其他缓存框架集成?
希望今天的分享对大家有所帮助!
使用WeakReference,实现Value失效,解决内存泄漏问题
ReferenceQueue/Cleaner辅助,提供更简洁、安全的实现方式
在缓存、会话管理等场景中,灵活应用失效机制