👉 HashMap 在极端情况下性能会“雪崩”吗?
👉 ConcurrentHashMap 的“分段锁”和“CAS”到底是什么?
👉 WeakHashMap 真的能帮你解决内存泄漏吗?
👉 IdentityHashMap 和 EnumMap 是什么“黑科技”?
HashMap 进阶:哈希冲突、扩容与性能陷阱HashMap 是性能王者,但其背后也隐藏着需要警惕的“暗礁”。
当多个 Key 的 hashCode() 计算出的哈希值,经过 (n-1) & hash 计算后,落在了同一个“宝箱”(桶)时,就发生了哈希冲突。
HashMap 通过在桶内维护一个链表来解决冲突。新元素被插入到链表头部(JDK 1.7)或尾部(JDK 1.8)。HashMap 的数组长度 >= 64 时,链表会“进化”为红黑树。 // treeifyBin 方法中的关键判断
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // MIN_TREEIFY_CAPACITY = 64
resize(); // 优先扩容
else
treeifyBin(tab, hash); // 转为红黑树🔍 哈希函数优化:hash() 方法的“扰动” 直接使用 key.hashCode() 可能导致高位信息丢失(因为 (n-1) & hash 只用到低位)。HashMap 的 hash() 方法通过“扰动”来减少碰撞:
static final int hash(Object key) {
int h;
// 将 hashCode 的高16位与低16位进行异或,让高位信息也参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}当 size > threshold(capacity * loadFactor)时,触发扩容。
newCap = oldCap << 1 (即 2 倍增长)。rehash 过程:遍历原数组的每个桶,将其中的 Node 重新计算在新数组中的位置。rehash 时,一个 Node 的新位置 index 要么是 原index,要么是 原index + 原数组长度。这个判断只需要检查 hash 值的一个特定 bit 位,避免了重复的 hash 计算和取模运算,大大提升了扩容效率。什么是哈希风暴? 当攻击者精心构造大量 Key,使得它们的 hashCode() 都不同,但经过 (n-1) & hash 计算后,全部落在同一个桶中,形成一个超长链表。
get、put、remove 操作的性能从 O(1) 退化到 O(n),HashMap 几乎瘫痪,可能导致服务拒绝(DoS)。hashCode:如 java.lang.String 在某些版本中引入了随机化种子。Key 进行校验或限制。ConcurrentHashMap 进阶:从“分段锁”到“CAS”的并发艺术ConcurrentHashMap 是高并发场景的基石,其设计思想是减少锁的粒度。
Segment 分段锁Map 逻辑上划分为多个 Segment(段),每个 Segment 继承自 ReentrantLock,是一个独立的、类似 Hashtable 的结构。Map 降低到单个 Segment。不同 Segment 上的操作可以并发进行。Segment 数量在初始化时固定(concurrencyLevel),无法动态调整。synchronized 桶锁JDK 1.8 彻底重构,回归单一的 Node<K,V>[] table,并采用更细粒度的同步策略。
table 的 Node 为 null 时,使用 Unsafe 类的 compareAndSwap 操作进行无锁插入。这是无锁化的关键。synchronized 锁桶:当桶内有元素(链表或红黑树)时,对 table[i] 这个 Node 对象加 synchronized 锁。锁的粒度降低到单个桶。volatile 关键字:table 数组和 Node 的 val、next 字段用 volatile 修饰,保证多线程间的可见性。get)完全无锁,性能极高;写操作锁粒度极小,支持更高的并发度。size() 与 mappingCount():精确与近似size():返回一个 int。为了保证返回值小于等于 Integer.MAX_VALUE,它需要对所有 Segment(JDK 1.7)或 CounterCell 数组(JDK 1.8)的计数进行精确求和,可能涉及一定的同步开销。mappingCount():返回一个 long。在 JDK 1.8 中,它直接返回 sumCount(),这个值是近似精确的(CounterCell 的更新是 CAS 的,求和时可能有微小误差,但在绝大多数场景下足够精确),性能更高。当 Map 可能非常大时,推荐使用 mappingCount()。WeakHashMap、IdentityHashMap、EnumMapWeakHashMap —— 基于弱引用的“缓存管家”WeakHashMap 的 Key 是弱引用(WeakReference)。
Key 对象除了在 WeakHashMap 中被弱引用外,没有其他强引用时,这个 Key 会被垃圾回收器回收,随后其对应的 Entry 也会被自动从 Map 中移除。Key,从而清理缓存条目。Value 不是弱引用:Value 是强引用!如果 Value 很大或持有对 Key 的强引用,Entry 仍不会被清理。Map<ExpensiveObject, Metadata> cache = new WeakHashMap<>();
ExpensiveObject obj = new ExpensiveObject();
cache.put(obj, new Metadata());
// 当 obj 不再被其他地方引用时,GC 会回收它,cache 中的条目也会被自动清除IdentityHashMap —— 基于 == 的“身份管家”IdentityHashMap 使用 == 运算符,而非 equals() 方法来比较 Key。
Key 是同一个对象实例(内存地址相同)时,才认为它们相等。Key 对象的 equals() 和 hashCode() 方法被重写得不符合 Map 合约(虽然这本身就是个错误)。== 比 equals() 快)。HashMap 不同。IdentityHashMap<String, String> map = new IdentityHashMap<>();
String s1 = new String("key");
String s2 = new String("key");
map.put(s1, "value1");
map.put(s2, "value2"); // s1 和 s2 是不同对象,所以都能放入
System.out.println(map.size()); // 输出 2EnumMap —— 专为枚举优化的“极速管家”EnumMap 是专门为 enum 类型 Key 设计的高性能 Map。
EnumMap 内部使用一个 Object[] 数组。Key 的 ordinal() 值(枚举常量的序数)直接作为数组的索引。put、get、remove 都是 O(1) 的数组访问,速度极快。enum 常量的声明顺序。Key 不能为空。Key 是枚举类型时的唯一选择!性能远超 HashMap。enum Status { RUNNING, STOPPED, PAUSED }
Map<Status, String> statusMsg = new EnumMap<>(Status.class);
statusMsg.put(Status.RUNNING, "系统运行中");
statusMsg.put(Status.STOPPED, "系统已停止");
// get 操作就是 array[Status.RUNNING.ordinal()],快如闪电HashMap 为什么在链表长度大于8时转为红黑树?为什么还要数组长度>=64?答: 转红黑树是为了将最坏情况的查找时间从 O(n) 优化到 O(log n)。阈值8是基于泊松分布计算出的概率极低的临界点。要求数组长度>=64是为了避免在数组很小时就过早地转换,因为此时扩容(rehash)是更优的减少冲突的策略。两个条件必须同时满足。
ConcurrentHashMap 在 JDK 1.8 中是如何实现高并发的?答: JDK 1.8 放弃了 Segment 分段锁,采用更细粒度的同步。核心是 CAS 操作(用于无锁插入空桶)和 synchronized 锁单个桶(锁粒度降低到数组元素)。get 操作完全无锁,volatile 保证可见性。这种设计使得读操作性能极高,写操作并发度也大幅提升。
WeakHashMap 的 Key 什么时候会被回收?Value 有影响吗?答: 当 Key 对象除了在 WeakHashMap 中被弱引用外,没有其他任何强引用时,下一次 GC 运行时,该 Key 会被回收。Value 是强引用,如果 Value 持有对 Key 的强引用,会阻止 Key 被回收,导致 Entry 无法被清理。Value 本身不会被 WeakHashMap 的机制自动清理。
EnumMap 为什么性能这么高?答: EnumMap 的性能源于其极简的设计。它内部是一个数组,Key(枚举常量)的 ordinal() 值直接作为数组索引。put 和 get 操作就是简单的数组赋值和取值,没有任何哈希计算、冲突解决或对象比较的开销,因此性能是 O(1) 且常数时间极小。
场景 | 推荐实现 | 关键点 |
|---|---|---|
单线程,极致性能,无序 | HashMap | 注意哈希冲突、扩容、hashCode陷阱 |
单线程,需保持顺序 | LinkedHashMap / TreeMap | 前者插入/访问序,后者键有序 |
多线程,高并发 | ConcurrentHashMap | JDK 1.8+ 采用 CAS + 桶锁 |
内存敏感的缓存 | WeakHashMap | Key 为弱引用,依赖 GC 回收 |
Key 为枚举类型 | EnumMap | 基于数组,性能之王 |
基于对象身份映射 | IdentityHashMap | 使用 == 比较,非 equals |
🔚 最后一句话 Map 的进阶,是理解哈希冲突与红黑树“进化”的智慧,是洞悉 ConcurrentHashMap 从“分段”到“无锁”的并发艺术,是掌握 WeakHashMap、EnumMap 这些“特种兵”的独特用途。