ConcurrentHashMap 是 Java 并发包 (java.util.concurrent) 中提供的一个线程安全的高性能哈希表 实现。它是 HashMap 的并发版本,专为多线程环境设计,无需外部同步即可安全地在多个线程中同时读写。
类层次结构
完整的继承体系
java
// ConcurrentHashMap 的声明
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
// 实现细节...
}
// 类图关系
java.lang.Object
└── java.util.AbstractMap<K,V>
└── java.util.concurrent.ConcurrentHashMap<K,V>
实现的接口
-
Map<K,V>- 基础映射接口 -
ConcurrentMap<K,V>- 并发映射接口(扩展Map) -
Serializable- 可序列化接口
核心接口详解
1. Map 接口(基础契约)
ConcurrentHashMap 首先是一个 Map,提供了所有映射的基本操作:
java
// Map 核心方法(ConcurrentHashMap 全部实现)
interface Map<K,V> {
V put(K key, V value);
V get(Object key);
V remove(Object key);
boolean containsKey(Object key);
int size();
boolean isEmpty();
void clear();
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K,V>> entrySet();
// ... 其他方法
}
2. ConcurrentMap 接口(并发增强)
ConcurrentMap 在 Map 基础上增加了原子性复合操作:
java
// ConcurrentMap 扩展的方法(针对并发设计)
interface ConcurrentMap<K,V> extends Map<K,V> {
// 1. 仅当键不存在时插入
V putIfAbsent(K key, V value);
// 2. 仅当键存在且值匹配时删除
boolean remove(Object key, Object value);
// 3. 仅当键存在且旧值匹配时替换
boolean replace(K key, V oldValue, V newValue);
// 4. 替换指定键的值
V replace(K key, V value);
}
实现细节:继承 AbstractMap
AbstractMap 的作用
AbstractMap 提供了 Map 接口的骨架实现,简化了具体实现类的工作:
java
// AbstractMap 提供的基础实现
public abstract class AbstractMap<K,V> implements Map<K,V> {
// 1. 提供 entrySet() 的默认实现框架
public abstract Set<Entry<K,V>> entrySet();
// 2. 基于 entrySet() 实现其他方法
public V get(Object key) {
// 遍历 entrySet 查找键
}
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
public int size() {
return entrySet().size();
}
public boolean isEmpty() {
return size() == 0;
}
// ... 其他通用实现
}
ConcurrentHashMap 如何扩展 AbstractMap
ConcurrentHashMap 重写了几乎所有方法,因为并发场景需要特殊处理:
java
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V> {
// 1. 不依赖父类的 entrySet() 实现
// 2. 所有核心方法都是独立重新实现
// 3. 添加并发特有的字段和方法
// 重写 size() 方法(特殊并发处理)
public int size() {
// 不是简单的 entrySet().size()
// 而是分段/分桶统计的近似值
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
// 实现 ConcurrentMap 的原子方法
public V putIfAbsent(K key, V value) {
return putVal(key, value, true);
}
}
设计模式:模板方法模式
这种继承结构体现了模板方法模式:
-
AbstractMap:定义算法骨架
-
ConcurrentHashMap:提供具体实现,但为并发优化而大幅重写
为什么不直接实现 Map?
原因 1:代码复用
尽管 ConcurrentHashMap 重写了很多方法,但仍能复用 AbstractMap 的一些辅助方法:
java
// AbstractMap 中可复用的通用实现
public String toString() {
// 使用 entrySet() 遍历(ConcurrentHashMap 已实现)
}
public boolean equals(Object o) {
// 比较逻辑可复用
}
public int hashCode() {
// 基于 entrySet() 计算哈希值
}
原因 2:契约保证
继承 AbstractMap 表明它遵循 Map 的标准契约。
原因 3:工具类兼容
一些通用工具方法基于 AbstractMap 实现:
java
// Collections 中的工具方法
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
// 这些方法对 AbstractMap 子类有特殊优化
ConcurrentHashMap 的特殊设计
字段结构(Java 8+)
java
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> {
// 核心存储:volatile 保证可见性
transient volatile Node<K,V>[] table;
// 扩容时的新表
private transient volatile Node<K,V>[] nextTable;
// 基础计数器(用于 size())
private transient volatile long baseCount;
// 控制变量(多用途:大小、扩容等)
private transient volatile int sizeCtl;
// 视图(延迟初始化)
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
}
视图实现
ConcurrentHashMap 有自己的视图实现,而非使用 AbstractMap 的默认视图:
java
// 自定义的线程安全视图
static final class KeySetView<K,V> extends CollectionView<K,V,K>
implements Set<K>, java.io.Serializable {
// 弱一致性的迭代器
public final Iterator<K> iterator() {
return new KeyIterator();
}
}
// 通过这些方法返回视图
public KeySetView<K,V> keySet() {
KeySetView<K,V> ks;
if ((ks = keySet) != null) return ks;
return keySet = new KeySetView<>(this, null);
}
与其他并发容器的比较
| 类 | 继承关系 | 特点 |
|---|---|---|
ConcurrentHashMap |
AbstractMap → ConcurrentHashMap |
分段/桶锁,高并发 |
ConcurrentSkipListMap |
AbstractMap → ConcurrentSkipListMap |
跳表,有序,无锁读 |
Hashtable |
Dictionary → Hashtable |
全表锁,遗留类 |
Collections.synchronizedMap |
包装器模式 | 全表锁,包装现有 Map |
总结关系
java
Map (接口) <-- 实现契约
↑
ConcurrentMap (接口) <-- 添加原子操作
↑
ConcurrentHashMap (具体类)
↑
AbstractMap (抽象类) <-- 提供部分默认实现(但大部分被重写)
关键点:
-
ConcurrentHashMap是 一个Map,提供所有映射功能 -
它实现了
ConcurrentMap,增加了原子复合操作 -
它继承 了
AbstractMap,但为并发性能几乎重写了所有方法 -
这种设计既保持了接口兼容性,又允许针对并发场景深度优化
这就是为什么你可以在需要 Map 的地方使用 ConcurrentHashMap,同时获得线程安全的额外好处。
主要特点
1. 线程安全且高效
-
与
Hashtable或Collections.synchronizedMap()的全局锁不同,ConcurrentHashMap使用分段锁 (Java 7)或 CAS + synchronized(Java 8+)实现细粒度并发控制 -
读操作通常无需加锁(除了扩容等特殊情况)
2. 弱一致性迭代器
-
迭代器创建后不反映所有更新操作(与
HashMap的快速失败迭代器不同) -
不会抛出
ConcurrentModificationException
3. 不允许 null 键或值
- 避免二义性:
map.get(key)返回null时,无法区分是键不存在还是值为null
核心实现原理
Java 7 实现(分段锁)
java
// Java 7 的 ConcurrentHashMap 结构
public class ConcurrentHashMap<K, V> {
// 核心:Segment 数组
final Segment<K,V>[] segments;
static final class Segment<K,V> extends ReentrantLock {
// 每个 Segment 有自己的哈希表
HashEntry<K,V>[] table;
int count; // Segment 内元素数量
}
static final class HashEntry<K,V> {
final K key;
volatile V value;
final int hash;
final HashEntry<K,V> next;
}
}
-
将整个表分为多个段(Segment)
-
每个段独立加锁,不同段可并发操作
分段锁原理
java
// 简化的写入流程
public V put(K key, V value) {
int hash = hash(key);
// 1. 根据 hash 确定 Segment
int segmentIndex = (hash >>> segmentShift) & segmentMask;
Segment<K,V> segment = segments[segmentIndex];
// 2. 对该 Segment 加锁
segment.lock();
try {
// 3. 在 Segment 内部的哈希表中操作
int bucketIndex = (hash >>> segment.bucketShift) & segment.bucketMask;
HashEntry<K,V> first = segment.table[bucketIndex];
// ... 插入逻辑
} finally {
segment.unlock();
}
}
分段锁的特点:
-
默认 16 个段(可通过构造函数设置)
-
不同段可以并发写入
-
同一段内操作需要竞争锁
-
读操作通常无锁(volatile 保证可见性)
Java 8+ 实现(CAS + synchronized)
主要变革
Java 8 抛弃了分段锁,采用更细粒度的并发控制:
java
// Java 8 的 ConcurrentHashMap 核心结构
public class ConcurrentHashMap<K,V> {
// 单个哈希表(不再是分段)
transient volatile Node<K,V>[] table;
// 桶节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val; // volatile 保证可见性
volatile Node<K,V> next; // volatile
}
// 红黑树节点(当链表过长时转换)
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // 删除时需要取消链接
boolean red;
}
}
Java 8 的并发控制技术
1. CAS(Compare And Swap)无锁算法
java
// 使用 CAS 的典型场景
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
// 应用:插入新节点(桶为空时)
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // 插入成功
2. synchronized 锁单个桶
java
// 当桶不为空时,使用 synchronized 锁住链表头/树根
Node<K,V> f = tabAt(tab, i);
synchronized (f) { // 锁住桶的第一个节点
if (tabAt(tab, i) == f) { // 双重检查
// 链表操作或树操作
if (fh >= 0) { // 链表
// 遍历链表...
} else if (f instanceof TreeBin) { // 红黑树
// 树操作...
}
}
}
3. 扩容时的并发控制
java
// 扩容期间的特殊处理
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 多个线程可以协助扩容
// 每个线程处理一个桶区间
while (advance) {
// 分配桶区间给当前线程
}
// 转移节点
synchronized (f) { // 锁住原桶
// 将节点拆分到新表的两个桶中
}
}
ava 8 的具体并发场景
场景 1:读操作(完全无锁)
java
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
// 无锁读取
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0) // 红黑树或扩容中
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
场景 2:写操作(CAS + synchronized)
java
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) {
// CASE 1: 桶为空,使用 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;
// CASE 2: 桶不为空,锁住桶头节点
synchronized (f) {
if (tabAt(tab, i) == f) { // 双重检查
if (fh >= 0) { // 链表
// ... 链表插入/更新
}
else if (f instanceof TreeBin) { // 红黑树
// ... 树插入/更新
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); // 转换为红黑树
break;
}
}
}
// ... 更新计数等
}
锁的精确粒度:桶级别
java
// Java 8 ConcurrentHashMap 的数组结构
Node<K,V>[] table = new Node[16]; // 哈希表数组
// 每个桶可能是以下三种状态之一:
// 1. null - 空桶
// 2. Node - 链表头节点
// 3. TreeBin - 红黑树的包装节点(持有树根)
三种情况的具体锁机制
情况 1:桶为空(无锁 CAS 操作)
java
// 当桶为空时,使用 CAS 无锁插入
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 使用 CAS 原子操作插入新节点
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // 插入成功
}
✅ 无锁操作:CAS 保证原子性
情况 2:桶为链表(锁链表头节点)
java
// 当桶是链表时,锁住链表头节点
synchronized (f) { // f 是链表第一个节点
if (tabAt(tab, i) == f) { // 双重检查
// 遍历链表进行操作
for (Node<K,V> e = f;; ++binCount) {
// ... 插入、更新或删除节点
}
}
}
🔒 锁对象 :链表第一个 Node 对象
情况 3:桶为红黑树(锁 TreeBin 节点)
java
// 当桶是红黑树时,锁住 TreeBin 节点
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
synchronized (t) { // 锁住 TreeBin 对象
// 在红黑树中插入、查找或删除
TreeNode<K,V> p = t.putTreeVal(hash, key, value);
}
}
🔒 锁对象 :TreeBin 对象(不是树根 TreeNode)
关键设计细节
1. 为什么锁第一个节点而不是整个数组?
java
// 错误示例:全局锁(性能差)
synchronized (table) { // ❌ 锁住整个数组
// 所有操作串行化
}
// 正确示例:桶级别锁
synchronized (table[i]) { // ✅ 只锁一个桶
// 其他桶的操作可以并发进行
}
2. 双重检查锁定模式
java
Node<K,V> f = tabAt(tab, i); // 第一次读取
synchronized (f) { // 锁住 f
if (tabAt(tab, i) == f) { // 第二次检查(确保 f 仍然是桶头)
// 执行操作
}
}
3. 锁对象的具体身份
java
// 数组中的元素可能是:
// 1. Node(链表)
table[3] = new Node<>(hash, key, value, next);
// 2. TreeBin(红黑树的包装器)
table[5] = new TreeBin<>();
// 3. ForwardingNode(扩容时的特殊标记)
table[2] = new ForwardingNode<>(nextTable);
对比表格:Java 7 vs Java 8
| 特性 | Java 7(分段锁) | Java 8(CAS + synchronized) |
|---|---|---|
| 锁粒度 | 段级别(默认16段) | 桶级别(更细粒度) |
| 锁机制 | ReentrantLock |
synchronized + CAS |
| 数据结构 | 数组+链表 | 数组+链表+红黑树 |
| 读并发 | 无锁(volatile读) | 无锁(volatile读) |
| 写并发 | 不同段可并发写 | 不同桶可并发写 |
| 扩容 | 段内独立扩容 | 协助扩容(多线程协作) |
| 内存开销 | 较高(Segment对象) | 较低 |
| 性能 | 段竞争时下降 | 竞争更少,吞吐量更高 |
Java 8 的关键优化
1. 红黑树优化链表
-
当链表长度 > 8 且表大小 ≥ 64 时,转换为红黑树
-
查找复杂度从 O(n) 降为 O(log n)
2. 扩容并发协助
java
// 多个线程可以协助扩容
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int stride = (NCPU > 1) ? (n >>> 3) / NCPU : n;
// 每个线程处理一个桶区间,并行迁移
}
3. 计数机制优化
java
// 使用 CounterCell 数组分散计数争用
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
// 避免单个计数器的争用
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
实际性能对比
java
// 基准测试示例
public class Benchmark {
public static void main(String[] args) {
// Java 7: 在高并发写时,如果键都hash到同一个段,性能会下降
// Java 8: 更细粒度的锁,即使hash冲突,只要不在同一个桶,仍可并发
}
}
结论:
-
Java 7 :分段锁是粗粒度的并发控制,段数固定可能成为瓶颈
-
Java 8 :CAS + synchronized 是更细粒度 的控制,配合红黑树和协助扩容,在高并发场景下性能显著提升,特别是在哈希冲突较多时
这就是为什么 Java 8 的 ConcurrentHashMap 能支持更高的并发度,同时保持更好的性能表现。