ConcurrentHashMap原理与源码分析
1. 引言
在Java并发编程中,ConcurrentHashMap
是一个至关重要的数据结构,它解决了普通HashMap
在多线程环境下的线程安全问题,同时克服了Hashtable
和Collections.synchronizedMap()
在高并发场景下的性能瓶颈。本文将深入分析ConcurrentHashMap
的设计原理、内部实现和源码细节,帮助读者彻底理解这一高性能并发集合类。
2. 为什么需要ConcurrentHashMap?
2.1 HashMap的线程安全问题
在多线程环境下,HashMap
存在以下安全隐患:
- 数据不一致:多个线程同时修改可能导致最终结果不确定
- 死循环:在JDK 1.7中,并发扩容时可能导致链表形成环,从而引发死循环
- 数据丢失:并发插入时可能覆盖彼此的更新
示例代码:
// 线程不安全的HashMap示例
HashMap<String, Integer> unsafeMap = new HashMap<>();
// 多线程并发修改可能导致问题
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
unsafeMap.put("key" + i, i); // 可能与其他线程冲突
}
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
unsafeMap.put("key" + i, i + 1); // 可能与其他线程冲突
}
}).start();
// 最终结果不可预测
2.2 传统解决方案的局限
-
Hashtable
:- 使用
synchronized
方法确保线程安全 - 所有操作都锁定整个表,串行执行
- 在高并发场景下性能急剧下降
- 使用
-
Collections.synchronizedMap(HashMap)
:- 本质上与
Hashtable
类似,使用同步包装器 - 也是对整个Map加锁,导致线程间相互阻塞
- 本质上与
// 传统线程安全Map示例
Hashtable<String, Integer> table = new Hashtable<>();
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// 这两种方式在高并发下都会有性能问题,因为所有操作都需要获取同一把锁
2.3 ConcurrentHashMap的优势
ConcurrentHashMap
设计目标是在保证线程安全的同时,实现高并发性能:
- 分段锁设计(JDK 1.7):将Map分为多个段,每个段独立加锁
- 细粒度锁 + CAS操作(JDK 1.8):锁定桶(bin)级别,进一步提高并发性
- 读操作无锁:完全不加锁,提高读取性能
- 弱一致性:牺牲强一致性换取高性能,适合大多数场景
3. JDK 1.7中的ConcurrentHashMap实现
3.1 整体结构
JDK 1.7中的ConcurrentHashMap
采用分段锁(Segment
)机制:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {
/**
* Segment表数组,每个Segment相当于一个小的Hashtable
*/
final Segment<K,V>[] segments;
// 其他字段...
/**
* Segment静态内部类
*/
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table;
transient int count; // 元素计数
transient int modCount; // 修改计数
transient int threshold; // 扩容阈值
final float loadFactor; // 负载因子
// 其他字段和方法...
}
/**
* HashEntry静态内部类,存储键值对,形成链表
*/
static final class HashEntry<K,V> {
final K key; // 键是不可变的
final int hash; // hash值也是不可变的
volatile V value; // 值可能变化,使用volatile保证可见性
volatile HashEntry<K,V> next; // next指针也使用volatile修饰
// 构造函数和其他方法...
}
}
核心特点:
- 分段设计:默认16个
Segment
,每个Segment
独立加锁 - 继承结构:
Segment
继承自ReentrantLock
,直接作为锁使用 - 不可变性:
key
和hash
是final
的,保证线程安全 - 可见性:
value
和next
是volatile
的,保证可见性
3.2 源码分析:关键操作
3.2.1 初始化过程
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
// 参数检查...
// 计算segments数组的大小,必须是2的幂
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// segmentShift和segmentMask用于定位segment
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
// 设置每个segment的容量
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 创建segments数组
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // 使用Unsafe类初始化第一个Segment
this.segments = ss;
}
关键点:
concurrencyLevel
决定分段数量,默认为16- 分段数必须是2的幂,便于计算
- 使用
Unsafe
类初始化,提高性能
3.2.2 put操作
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key); // 计算hash值
// 计算segment索引
int j = (hash >>> segmentShift) & segmentMask;
// 使用Unsafe类获取segment,不存在则初始化
if ((s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
// 调用segment的put方法
return s.put(key, hash, value, false);
}
// Segment的put方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 获取锁
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
// 计算桶的索引
int index = (tab.length - 1) & hash;
// 获取桶中的第一个节点
HashEntry<K,V> first = entryAt(tab, index);
// 遍历链表
for (HashEntry<K,V> e = first; e != null; e = e.next) {
K k;
// 找到相同的key,更新值
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value; // 更新值
++modCount;
}
break;
}
node = e;
}
// 没有找到key,创建新节点
if (node == null) {
// 创建新节点并添加到链表头部
HashEntry<K,V> newNode = new HashEntry<K,V>(hash, key, value, first);
setEntryAt(tab, index, newNode);
++modCount;
// 元素计数增加
int c = count + 1;
// 检查是否需要扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(); // 扩容
count = c;
oldValue = null;
}
return oldValue;
} finally {
unlock(); // 释放锁
}
}
关键点:
- 先定位
Segment
,再在Segment
内部进行操作 - 使用
tryLock()
尝试获取锁,失败则调用scanAndLockForPut
scanAndLockForPut
会自旋获取锁,并预先构建节点,提高性能- 每个
Segment
独立管理自己的扩容
3.2.3 get操作
public V get(Object key) {
Segment<K,V> s; // 声明segment局部变量
HashEntry<K,V>[] tab;
int h = hash(key); // 计算hash值
// 计算segment索引
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// 先获取segment,如果segment存在且该segment的table不为空
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 在segment的table中查找,不需要加锁
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value; // 找到key,返回value
}
}
return null; // 未找到key,返回null
}
关键点:
get
操作完全不加锁,利用volatile
保证可见性- 使用
Unsafe.getObjectVolatile
方法获取最新的引用值 - 由于
key
和hash
是final
的,value
和next
是volatile
的,保证了读取的线程安全性
3.3 JDK 1.7实现的优缺点
优点:
- 分段锁设计大幅提高了并发性能
- 实现了所有
ConcurrentMap
接口定义的原子操作 - 迭代器不会抛出
ConcurrentModificationException
缺点:
- 分段锁粒度仍然较粗
Segment
数量一旦初始化就无法改变- 计算
Segment
索引的过程稍复杂,有一定性能开销
4. JDK 1.8中的ConcurrentHashMap实现
4.1 整体结构
JDK 1.8彻底重构了ConcurrentHashMap
:
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V> {
// 最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始容量
private static final int DEFAULT_CAPACITY = 16;
// 负载因子
private static final float LOAD_FACTOR = 0.75f;
// 树化阈值,链表长度超过8转为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 树退化阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 树化的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// Node数组,存储数据
transient volatile Node<K,V>[] table;
// 下一个要使用的table,只有在扩容时非空
private transient volatile Node<K,V>[] nextTable;
// 计数基值
private transient volatile long baseCount;
// 计数器数组,用于高并发计数
private transient volatile CounterCell[] counterCells;
// 扩容控制标记
private transient volatile int sizeCtl;
// 其他字段...
}
核心改变:
- 移除
Segment
:不再使用分段锁设计 - 采用
Node
数组:与HashMap
结构相似 - 使用
synchronized
和CAS
:实现细粒度的锁控制 - 支持红黑树:当链表过长时转为红黑树,提高查找性能
- 复杂的计数器设计:使用
baseCount
加CounterCell
数组,提高并发计数性能
4.2 关键内部类
// 基本节点类,存储key-value对
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保证可见性
// 构造函数和Entry接口方法...
// 查找方法
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
// 树节点类,红黑树结构
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; // 红黑树颜色标记
// 树操作相关方法...
}
// 转发节点,仅在扩容期间使用
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable; // 指向扩容后的新表
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null); // MOVED是一个特殊的hash值
this.nextTable = tab;
}
// 在nextTable中查找
Node<K,V> find(int h, Object k) {
// 使用特殊算法在nextTable中查找
}
}
// 空值占位符
static final class ReservationNode<K,V> extends Node<K,V> {
ReservationNode() {
super(RESERVED, null, null, null); // RESERVED是特殊hash值
}
Node<K,V> find(int h, Object k) {
return null;
}
}
4.3 源码分析:关键操作
4.3.1 初始化过程
// 默认构造函数
public ConcurrentHashMap() {
}
// 带初始容量的构造函数
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap; // sizeCtl保存初始容量
}
// 真正的初始化在第一次put操作时进行
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0) // 有其他线程在初始化
Thread.yield(); // 暂让出CPU时间
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // CAS修改sizeCtl为-1,标记正在初始化
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 使用sizeCtl或默认容量
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // 创建Node数组
table = tab = nt;
sc = n - (n >>> 2); // sc = 0.75n,设置扩容阈值
}
} finally {
sizeCtl = sc; // 恢复sizeCtl为扩容阈值
}
break;
}
}
return tab;
}
关键点:
- 延迟初始化:只有在第一次使用时才真正分配内存
- 使用
CAS
保证只有一个线程执行初始化 sizeCtl
是一个控制标识符:sizeCtl
> 0:表示下一次扩容的阈值sizeCtl
= -1:表示正在初始化sizeCtl
< -1:表示有-sizeCtl-1
个线程正在进行扩容sizeCtl
= 0:默认值
4.3.2 put操作
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); // 计算hash值
int binCount = 0; // 桶中的节点计数
for (Node<K,V>[] tab = table;;) { // 自旋
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 1. 初始化表
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 2. 桶为空,直接CAS插入
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // 无竞争,CAS成功,退出循环
}
else if ((fh = f.hash) == MOVED) // 3. 发现ForwardingNode,说明正在扩容
tab = helpTransfer(tab, f); // 帮助扩容
else { // 4. 桶不为空,且不在迁移,锁定头节点
V oldVal = null;
synchronized (f) { // 对头节点加锁
if (tabAt(tab, i) == f) { // 检查头节点是否被修改
if (fh >= 0) { // 普通链表节点
binCount = 1;
for (Node<K,V> e = f;; ++binCount) { // 遍历链表
K ek;
// 找到相同的key,更新value
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value; // 更新value
break;
}
Node<K,V> pred = e;
// 到达链表尾部,创建新节点
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
else if (f instanceof TreeBin) { // 红黑树节点
Node<K,V> p;
binCount = 2;
// 调用红黑树的putTreeVal方法
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
// 检查是否需要转为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 增加计数
addCount(1L, binCount);
return null;
}
关键点:
- 使用自旋+CAS解决竞争问题
- 只对链表/树的头节点加锁,细粒度锁
- 发现正在扩容则帮助扩容
- 链表过长转换为红黑树
- 计数器使用复杂的
addCount
方法,支持高并发计数
4.3.3 get操作
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算hash值
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; // 第一个节点就是要找的,直接返回
}
// 处理特殊节点(如ForwardingNode、TreeBin)
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; // 未找到
}
关键点:
get
方法完全无锁,依靠volatile
变量保证可见性- 通过
tabAt
方法获取最新的桶头节点 - 优先检查第一个节点,然后是特殊节点,最后是链表
4.3.4 扩容过程
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 计算每个线程负责迁移的桶区间
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
// 如果nextTab为空,初始化新表,容量翻倍
if (nextTab == null) {
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE; // 扩容失败,把sizeCtl设为最大值
return;
}
nextTable = nextTab;
transferIndex = n; // 初始化转移索引
}
int nextn = nextTab.length;
// 创建ForwardingNode,标记已经处理过的桶
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; // 是否继续向前处理下一个桶
boolean finishing = false; // 是否完成扩容
// i是当前处理的桶索引,bound是边界
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 确定本线程负责的桶区间
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 检查是否完成扩容
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { // 所有桶都处理完了
nextTable = null;
table = nextTab; // 更新table引用
sizeCtl = (n << 1) - (n >>> 1); // 更新sizeCtl为新容量的0.75倍
return;
}
// 当前线程完成任务,递减扩容线程计数
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return; // 不是最后一个线程,直接返回
finishing = advance = true; // 是最后一个线程,设置finishing为true
i = n; // 重新检查一遍
}
}
else if ((f = tabAt(tab, i)) == null) // 桶为空
advance = casTabAt(tab, i, null, fwd); // CAS设置ForwardingNode
else if ((fh = f.hash) == MOVED) // 已经是ForwardingNode
advance = true;
else { // 需要迁移的桶
synchronized (f) { // 加锁
if (tabAt(tab, i) == f) { // 再次检查头节点是否被修改
Node<K,V> ln, hn; // 低位链表和高位链表
if (fh >= 0) { // 普通链表节点
// 根据扩容后的哈希位确定节点位置
int runBit = fh & n; // n是旧容量,用于确定高位的0/1
Node<K,V> lastRun = f;
// 找到最后一段全是0或全是1的子链表
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 根据runBit分配低位链表或高位链表
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
// 遍历链表,构建低位和高位两个链表
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p