引言
在实际开发中,我们经常用到缓存,常见的就是使用
Redis
,memcached
等;但是实际还有许许多多场景,我们也想做缓存,但是数据又是可以随着资源紧张来丢弃的;这个时候使用Redis
和memcached
就有点繁琐了,那么今天我们来认识一个做内存缓存的利器:WeakHashMap
一、认识WeakHashMap
从上面的图,我们可以看出,WeakHashMap
也是我们集合的大家族里面的一员,所以他是满足Map
这里面的基本特性的,那我们具体看看他自己有什么特点,如何工作的呢?
1.1 先回顾一下HashMap的工作原理和特性
特性类别 | 描述 |
---|---|
工作原理 | |
哈希表 | HashMap基于哈希表实现,存储键值对。 |
哈希函数 | 通过哈希函数计算键的哈希值,确定键值对在哈希表中的存储位置。 |
处理冲突 | 使用链表或红黑树(Java 8及以后版本)来解决哈希冲突。 |
动态扩容 | 元素数量达到一定阈值时,自动进行扩容,重新计算所有元素位置。 |
特性 | |
允许空键和空值 | HashMap允许键或值为null。 |
非同步 | 不是线程安全的。可以通过Collections.synchronizedMap或ConcurrentHashMap实现线程安全。 |
迭代顺序 | 迭代顺序是插入顺序,直到结构上发生变化(如插入或删除操作)。 |
性能 | 提供接近常数时间的性能(平均情况下),最坏情况下退化到线性时间。 |
初始容量和加载因子 | 影响HashMap的性能和空间消耗。初始容量是哈希表创建时的容量,加载因子是容量自动增加前的满度阈值。 |
1.2 瞧瞧WeakHashMap的特点
-
键是弱引用:键可能在不被使用时自动从映射中消失。
-
不保证映射有序:迭代
WeakHashMap
时元素的顺序是不确定的,因为垃圾回收可能在任何时候移除键。 -
快速失败迭代器:
WeakHashMap
的迭代器是一个快速失败迭代器,如果映射结构在迭代过程中被修改,迭代器会快速失败。
1.3 WeakHashMap的工作原理
特性 | 描述 |
---|---|
存储机制 | WeakHashMap 是基于哈希表的Map实现,内部使用一个Entry 数组存储键值对。每个Entry 是一个单向链表节点,包含键值对和指向下一个节点的引用。 |
键(Key )的弱引用 | 键是弱引用的,如果没有被其他地方强引用,垃圾收集器可以回收该键及其关联的值。一旦键被回收,其在Map 中的条目也会被自动移除。 |
值(Value )的存储 | 值是强引用的,即使键被回收,如果没有其他地方引用这些值,它们也不会被垃圾收集器回收。值一直被保留,直到被显式移除或其键被回收。 |
二、 何时使用WeakHashMap
前面我们已经认识了WeakHashMap
,那我们现在来看看如何使用它呢?直接上案例:
import org.apache.poi.ss.formula.functions.T;
import javax.imageio.ImageIO;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.WeakHashMap;
public class WeekHashMapCache {
private static Map<String, Object> cache = Collections.synchronizedMap(new WeakHashMap<>());
/**
* 添加缓存
*
* @param key 键
* @param obj 值
*/
public static void add(String key, Object obj) {
cache.put(key, obj);
}
/**
* 查询缓存
*
* @param key 键
* @param clazz 泛型对象
* @return 具体值
*/
public static Optional<T> get(String key, Class<T> clazz) {
Object o = cache.get(key);
if (Objects.isNull(o)) {
return Optional.empty();
}
try {
return Optional.ofNullable(clazz.cast(o));
} catch (ClassCastException e) {
// 如果类型转换失败,返回 Optional.empty()
return Optional.empty();
}
}
public static void main(String[] args) throws IOException, InterruptedException {
String imageKey = "test001";
Image image = ImageIO.read(new File("C:\Users\Admin\Pictures\wztt\zl\Snipaste_2024-08-16_17-59-55.png"));
// 1. 缓存图片(imageKey是强引用)
add(imageKey, image);
// 输出
System.out.println("数据存在: " + cache.get(imageKey));
}
}
特别要注意的是,若 Value
对象反向持有 Key
的强引用(如 map.put(key, key.toString())
),会导致内存泄漏,需避免此设计。
三、 WeakHashMap的局限性
实际看了上面的工作原理和简单案例,大家应该也发现了WeakHashMap
有一些天然的局限,下面我们来仔细看看。
3.1 键回收依赖垃圾回收机制,不可控
-
WeakHashMap
的键通过弱引用关联,回收时机完全由 JVM 的垃圾回收器决定,开发者无法主动控制。即使调用System.gc()
也仅是建议,无法保证立即回收。 -
表现示例:若键不再被强引用但尚未触发 GC,条目仍会占用内存,导致内存释放延迟。
3.2 Value 未弱引用,可能导致内存泄漏
-
WeakHashMap
仅对键使用弱引用,Value 仍保持强引用。若 Value 直接或间接强引用 Key(如Value = key.toString()
),即使 Key 被回收,Value 仍无法释放。 -
典型场景:缓存系统中,若 Value 包含对 Key 的反向引用,会导致内存泄漏。
3.3 线程不安全,需额外同步
-
WeakHashMap
非线程安全,多线程并发修改可能导致数据不一致。需通过Collections.synchronizedMap
包装或手动加锁。 -
问题示例:并发调用
put()
和get()
时,可能触发ConcurrentModificationException
。
3.4 不适合长期缓存场景
-
键可能随时被回收的特性导致
WeakHashMap
不适合需要长期保留数据的缓存。若缓存项被意外回收,需频繁重新加载数据,影响性能。 -
对比方案:长期缓存建议使用
ConcurrentHashMap
或Guava Cache
。
3.5 性能开销与不确定性
-
内部维护 引用队列(ReferenceQueue) 和自动清理机制会引入额外性能开销,高频操作时影响效率。
-
不确定性表现:调用
size()
或isEmpty()
时结果可能不准确(清理操作非实时触发)。
3.6 键为 null
时的特殊行为
-
WeakHashMap
允许键为null
,但若显式使用null
作为键,可能导致条目无法被正常清理或引发逻辑错误。 -
示例风险:
map.put(null, value)
后,若其他逻辑依赖键非空,可能触发空指针异常。
四、最佳实践
在第二点里面我给了一个使用WeakHashMap
的案例,但是第三点我又写了很多使用的额局限性,这些局限性实际暴露了很多问题,如果一不小心,就会爆出一个大雷,那么怎么样才能更好的使用呢?
首先我们要理解,WeakHashMap
的核心就是使用弱引用来做内存缓存,下面我将给出一个变体的解决方案。
Cache<String, String> cache = CacheBuilder.newBuilder()
.weakValues() // 值使用弱引用
.maximumSize(1000) // 额外容量限制
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入10分钟后过期
.build();
五、总结
可能有人发现第四点解决方案最终不是使用的WeakHashMap
,确实是,这篇文章实际写到这里,相信大家也理解了,当自己把握不住的时候,找一个权威的三方组件,就是最好的办法,当然能够自己研究弄透彻更好。
希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。