HashMap 和 ConcurrentHashMap的区别
一、HashMap:非线程安全的哈希表实现
1. 核心数据结构
// 简化版源码(Java 8+)
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 Node<K,V> {TreeNode<K,V> parent; // 红黑树结构TreeNode<K,V> left;TreeNode<K,V> right;boolean red;
}
-
数组 + 链表/红黑树:默认容量 16,负载因子 0.75
-
树化条件:链表长度 ≥8 且桶数组长度 ≥64
-
退化条件:树节点数 ≤6
2. 关键操作原理
扩容机制(Java 8 优化)
-
新位置 = 原位置 OR 原位置 + 旧容量
-
通过
(e.hash & oldCap) == 0
判断位置:if ((e.hash & oldCap) == 0) {newTab[j] = loHead; // 保持原索引 } else {newTab[j + oldCap] = hiHead; // 新索引 = 原索引 + 旧容量 }
3. 线程安全问题
问题类型 | 原因说明 |
---|---|
数据覆盖 | 多线程同时 put 时覆盖值(非原子操作) |
死循环 | Java 7 头插法扩容导致链表成环(Java 8 尾插法解决) |
size 不准确 | 多线程并发修改 size 计数器 |
迭代器 fail-fast | 并发修改触发 ConcurrentModificationException |
二、ConcurrentHashMap:线程安全的高并发实现
1. 演进历史
-
Java 7:分段锁(Segment),默认 16 段
-
Java 8+:CAS + synchronized + volatile(桶级别锁)
2. Java 8 底层实现
(1) 核心数据结构
transient volatile Node<K,V>[] table; // volatile 保证可见性static class Node<K,V> implements Map.Entry<K,V> {volatile V val; // volatile 修饰值volatile Node<K,V> next; // volatile 修饰指针
}
-
禁止 null 键值:避免并发歧义
-
节点锁:synchronized 锁定单个桶的首节点
(2) 线程安全机制
机制 | 应用场景 | 实现方式 |
---|---|---|
CAS | 初始化数组、插入空桶、计数更新 | U.compareAndSwapObject() |
synchronized | 锁定非空桶 | synchronized (f) { ... } (f 是桶的首节点) |
volatile | 保证内存可见性 | Node 的 val/next 字段声明为 volatile |
ForwardingNode | 扩容时安全访问 | 特殊节点标记正在迁移的桶 |
3. 高并发优化设计
-
多线程协同扩容
-
线程 put 时发现桶为
ForwardingNode
,会协助迁移数据 -
通过
transferIndex
分配迁移任务区间
-
-
高效计数机制
private transient volatile CounterCell[] counterCells;
-
类似 LongAdder 的分段计数,减少 CAS 竞争
-
-
查询免锁
-
依赖 volatile 和 Unsafe.getObjectVolatile 保证可见性
-
遍历过程无需加锁
-
三、HashMap vs ConcurrentHashMap 对比
特性 | HashMap | ConcurrentHashMap (Java 8) |
---|---|---|
线程安全 | ❌ 非线程安全 | ✅ 桶级别锁 + CAS |
锁粒度 | 无锁 | 单个桶首节点 (synchronized) |
Null 支持 | ✅ 允许 null 键/值 | ❌ 禁止 null(避免并发歧义) |
迭代器 | Fail-Fast | 弱一致性迭代器 |
扩容机制 | 单线程扩容 | 多线程协同扩容 |
计数实现 | size 变量 | CounterCell 分段计数 |
树化阈值 | 链表长度 ≥8 | 同 HashMap |
内存可见性 | 无特殊保障 | volatile + Unsafe 操作 |
死锁风险 | 无锁无死锁 | 桶级别锁,无死锁风险 |
适用场景 | 单线程/只读多线程 | 高并发读写场景 |
四、关键问题解析
1. 为什么 ConcurrentHashMap 不允许 null?
-
并发歧义问题:无法区分 "key 不存在" 还是 "key 对应的值为 null"
-
设计一致性:Map.get(key) 返回 null 时,在并发环境下无法确定真实含义
2. 为什么 Java 8 抛弃分段锁?
-
内存开销:Segment 数组消耗额外内存
-
伪共享问题:CPU 缓存行失效影响性能
-
锁粒度粗:同 Segment 不同桶仍会竞争锁
-
桶级别锁优势:
-
碰撞概率低时几乎无锁竞争
-
synchronized 随 JVM 优化性能提升
-
3. 如何保证 size() 的准确性?
-
分治计数:
final long sumCount() {CounterCell[] cs = counterCells;long sum = baseCount;if (cs != null) {for (CounterCell c : cs)sum += c.value;}return sum; }
-
更新时优先尝试 CAS 修改
baseCount
-
竞争激烈时分散到
CounterCell[]
中 -
最终结果为所有分片求和
-
五、最佳实践建议
-
选型指南
-
单线程/只读场景 →
HashMap
-
高并发读写 →
ConcurrentHashMap
-
强一致性需求 →
Collections.synchronizedMap()
-
-
性能优化
-
预分配容量:避免扩容开销
-
避免可变对象作为 Key:防止哈希值变化
-
复杂对象:重写
hashCode()
和equals()
-
-
并发使用注意
-
迭代器弱一致性:不保证实时最新数据
-
批量操作:使用
forEach
/search
/reduce
并行方法
-
示例场景:实现高并发缓存
ConcurrentHashMap<String, AtomicLong> cache = new ConcurrentHashMap<>();// 原子性累加 cache.computeIfAbsent("counter", k -> new AtomicLong()).incrementAndGet();