HashMap底层原理详解:扩容、红黑树与ConcurrentHashMap的线程安全
HashMap作为Java集合框架中最重要且最常用的数据结构之一,其底层实现原理和线程安全方案是Java开发者必须掌握的核心知识。本文将深入剖析HashMap的实现机制,并详细解释ConcurrentHashMap如何保证线程安全。
一、HashMap底层数据结构
1.1 数组+链表+红黑树结构
HashMap在JDK 1.8后的底层实现采用"数组+链表+红黑树"的混合结构:
java
// HashMap中的核心数组定义 transient Node<K,V>[] table;// 链表节点定义 static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;// ... }// 红黑树节点定义 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent; // 红黑树链接TreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev; // 删除后需要取消链接boolean red;// ... }
1.2 哈希计算与索引定位
HashMap通过哈希函数确定键值对的存储位置:
java
// 计算key的哈希值 static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }// 计算数组下标 int index = (table.length - 1) & hash(key);
高16位与低16位异或运算的目的是为了增加哈希值的随机性,减少哈希冲突。
二、HashMap扩容机制
2.1 扩容触发条件
HashMap在以下情况下会触发扩容:
初始化后首次插入元素:默认创建长度为16的数组
元素数量超过阈值:阈值 = 容量 × 负载因子(默认0.75)
链表长度达到8但数组长度小于64:优先扩容而不是树化
2.2 扩容过程
扩容过程主要分为以下步骤:
java
final Node<K,V>[] resize() {// 1. 计算新容量和新阈值int newCap = oldCap << 1; // 双倍扩容float newThr = oldThr << 1;// 2. 创建新数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 3. 迁移元素(重新哈希)for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {// 处理每个桶的元素迁移if (e.next == null) {// 单个节点直接迁移newTab[e.hash & (newCap - 1)] = e;} else if (e instanceof TreeNode) {// 红黑树迁移((TreeNode<K,V>)e).split(this, newTab, j, oldCap);} else {// 链表迁移(使用高低位链表优化)Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;// ...}}}return newTab; }
2.3 扩容优化:高低位链表
JDK 1.8对链表迁移进行了优化,通过判断(e.hash & oldCap) == 0将原链表拆分为低位链表和高位链表:
低位链表:保持原索引位置不变
高位链表:新索引 = 原索引 + 原容量
这样避免了重新计算哈希值,提高了扩容效率。
三、红黑树转化条件
3.1 链表转红黑树
当同时满足以下两个条件时,链表会转化为红黑树:
链表长度达到阈值8
数组长度达到最小树化容量64(否则优先扩容)
java
// 树化阈值 static final int TREEIFY_THRESHOLD = 8;// 最小树化容量 static final int MIN_TREEIFY_CAPACITY = 64;final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;// 如果数组长度小于64,优先扩容而不是树化if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();else if ((e = tab[index = (n - 1) & hash]) != null) {// 执行树化操作TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null)hd.treeify(tab);} }
3.2 红黑树退化为链表
当同时满足以下两个条件时,红黑树会退化为链表:
树节点数量小于等于退化阈值6
在扩容 resize 或删除 remove 操作时触发检查
java
// 退化阈值 static final int UNTREEIFY_THRESHOLD = 6;// 在树节点删除后检查是否需要退化 if (root == null || root.right == null ||(rl = root.left) == null || rl.left == null) {tab[index] = first.untreeify(map); // 退化return; }
四、ConcurrentHashMap线程安全实现
4.1 JDK 1.7分段锁机制
在JDK 1.7中,ConcurrentHashMap使用分段锁(Segment)实现线程安全:
java
// 分段锁结构 final Segment<K,V>[] segments;static final class Segment<K,V> extends ReentrantLock implements Serializable {// 每个Segment独立管理一个HashEntry数组transient volatile HashEntry<K,V>[] table; }// 操作时只需要锁住对应的Segment public V put(K key, V value) {Segment<K,V> s;// 只锁定当前Segment,不影响其他Segment的操作int hash = hash(key);int j = (hash >>> segmentShift) & segmentMask;s = ensureSegment(j);return s.put(key, hash, value, false); }
4.2 JDK 1.8 CAS+synchronized优化
JDK 1.8放弃了分段锁,采用更细粒度的锁机制:
java
// 使用CAS和synchronized保证线程安全 final V putVal(K key, V value, boolean onlyIfAbsent) {// ...for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0)tab = initTable(); // 使用CAS初始化else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// 桶为空,使用CAS添加新节点if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))break;}else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f); // 协助扩容else {V oldVal = null;synchronized (f) { // 锁住桶的头节点// 执行链表或树操作if (tabAt(tab, i) == f) {if (fh >= 0) {// 链表操作} else if (f instanceof TreeBin) {// 红黑树操作}}}// ...}} }
4.3 关键线程安全技术
CAS操作:用于无竞争情况下的快速操作
数组初始化
空桶节点插入
计数器的更新
synchronized锁:用于锁定单个桶(链表头节点或树根节点)
粒度更细,并发度更高
与CAS配合实现高效并发
volatile变量:保证内存可见性
table数组引用
节点next指针
sizeCtl等控制变量
多线程协同扩容:
当前线程插入时发现正在扩容,会协助迁移数据
通过ForwardingNode节点标识正在迁移的桶
五、HashMap与ConcurrentHashMap对比
特性 | HashMap | ConcurrentHashMap |
---|---|---|
线程安全 | 否 | 是 |
锁粒度 | 无锁(非线程安全) | 桶级别锁(JDK1.8) |
空键值 | 允许一个null键和多个null值 | 不允许null键或值 |
迭代器 | Fail-Fast | Weakly Consistent |
性能 | 单线程最优 | 高并发下性能优异 |
六、实践建议
合理设置初始容量和负载因子:避免频繁扩容
java
// 预估元素数量,避免重复扩容 Map<String, Object> map = new HashMap<>(expectedSize, 0.75f);
键对象实现规范哈希方法:
java
// 重写hashCode和equals方法 public class Key {@Overridepublic int hashCode() {// 保证哈希分布均匀}@Overridepublic boolean equals(Object obj) {// 保证一致性} }
高并发场景使用ConcurrentHashMap:替代Hashtable和Collections.synchronizedMap
关注树化退化阈值:理解8和6这两个关键数字的意义
总结
HashMap通过数组+链表+红黑树的混合结构实现了高效的查找和插入操作,其扩容机制和树化策略在时间和空间上达到了良好平衡。ConcurrentHashMap则通过CAS+synchronized的细粒度锁机制,在保证线程安全的同时提供了优异的并发性能。理解这些底层原理不仅有助于正确使用这些数据结构,也能为设计和优化高性能Java应用奠定坚实基础。
对于Java开发者而言,深入理解HashMap和ConcurrentHashMap的底层实现是提升技术深度的必经之路,也是应对高级别技术面试的关键准备。