Appearance
集合-ConcurrentHashMap源码解析
一、为什么需要ConcurrentHashMap?
在并发场景下,HashMap
(非线程安全)会出现死循环(JDK1.7链表扩容时的循环引用)、数据覆盖等问题;Hashtable
(全局synchronized
锁)和Collections.synchronizedMap
(包装HashMap
,全局锁)则因锁粒度太大,导致并发性能低下(同一时间只能有一个线程操作Map)。
ConcurrentHashMap
的目标是在保证线程安全的前提下,最大化并发效率,其核心策略是:
- 减少锁粒度(分段锁→节点锁);
- 无锁操作(CAS);
- 高效数据结构(链表→红黑树);
- 多线程协作(扩容时协助转移节点)。
二、JDK1.7的ConcurrentHashMap:分段锁(Segment)
JDK1.7的ConcurrentHashMap
采用Segment数组+HashEntry链表的结构,每个Segment
本质是一个小型HashMap
,并继承自ReentrantLock
(可重入锁)。
1. 核心结构
java
// ConcurrentHashMap的顶级结构:Segment数组
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
final Segment<K,V>[] segments; // 分段数组
final int segmentMask; // 分段掩码(用于计算Segment索引)
final int segmentShift; // 分段移位(用于计算Segment索引)
// ...
}
// 每个Segment是一个可重入锁,内部包含HashEntry链表
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table; // 哈希表(链表数组)
transient int count; // 当前Segment的元素数量
transient int modCount; // 修改次数(用于快速失败,但ConcurrentHashMap迭代是弱一致的)
transient int threshold; // 扩容阈值(table.length * loadFactor)
final float loadFactor; // 负载因子
// ...
}
// HashEntry:链表节点,存储键值对
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value; // volatile修饰,保证可见性
volatile HashEntry<K,V> next; // volatile修饰,保证链表结构的可见性
// ...
}
2. 核心操作分析
(1)put操作:分段加锁
put
操作的核心是找到对应的Segment,对该Segment加锁,再执行类似HashMap
的插入逻辑。
java
public V put(K key, V value) {
if (value == null) throw new NullPointerException();
int hash = hash(key); // 计算key的hash值
// 计算Segment索引:(hash >>> segmentShift) & segmentMask
int segmentIndex = (hash >>> segmentShift) & segmentMask;
// 获取对应的Segment(若未初始化则初始化)
Segment<K,V> s = ensureSegment(segmentIndex);
// 对Segment加锁,执行插入
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链表的索引
HashEntry<K,V> first = tab[index]; // 链表头节点
for (HashEntry<K,V> e = first;;) {
if (e != null) { // 链表非空,遍历查找
K k = e.key;
if (e.hash == hash && (k == key || key.equals(k))) { // 找到相同key
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value; // 修改值(volatile保证可见性)
modCount++;
}
break;
}
e = e.next;
} else { // 链表为空,插入新节点
if (node != null) {
node.next = first;
} else {
node = new HashEntry<>(hash, key, value, first);
}
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY) { // 超过阈值,扩容(Segment内部扩容)
rehash(node);
} else {
tab[index] = node; // 插入链表头
}
count = c;
modCount++;
oldValue = null;
break;
}
}
} finally {
unlock(); // 释放锁
}
return oldValue;
}
关键细节:
- 分段锁:每个Segment独立加锁,不同Segment的操作可以并发执行(比如线程1操作Segment 0,线程2操作Segment 1,互不阻塞);
- volatile保证可见性:
HashEntry
的value
和next
用volatile
修饰,确保修改后其他线程能立即看到; - 扩容:Segment内部扩容(仅扩容当前Segment的
table
),不会影响其他Segment。
(2)get操作:无锁
get
操作不需要加锁,因为HashEntry
的value
和next
是volatile
的,保证了可见性。
java
public V get(Object key) {
int hash = hash(key);
// 计算Segment索引,获取对应的Segment
Segment<K,V> s = segmentForHash(hash);
// 调用Segment的get方法
return s == null ? null : s.get(key, hash);
}
// Segment的get方法
final V get(Object key, int hash) {
if (count == 0) return null; // 快速失败(Segment为空)
HashEntry<K,V> e = getFirst(hash); // 获取链表头节点
while (e != null) {
if (e.hash == hash && (e.key == key || key.equals(e.key))) { // 找到目标节点
return e.value;
}
e = e.next;
}
return null;
}
关键细节:
get
操作无锁,因为volatile
保证了value
的可见性,且链表结构的修改(next
)也会被立即看到;- 弱一致性:迭代时若有其他线程修改Map,迭代器不会抛出
ConcurrentModificationException
,但会反映最新状态(因为next
是volatile
的)。
(3)size计算:分段累加
size
操作需要累加所有Segment的count
值,但并发场景下count
可能随时变化,因此需要重试机制:
java
public int size() {
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // 是否溢出
long sum; // 总元素数
long last = 0L; // 上一次的总和
int retries = -1; // 重试次数(初始为-1,表示第一次尝试)
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) { // 重试超过3次,对所有Segment加锁
for (int i = 0; i < segments.length; i++) {
segments[i].lock();
}
}
sum = 0L;
overflow = false;
for (int i = 0; i < segments.length; i++) {
sum += segments[i].count; // 累加每个Segment的count
}
if (sum == last) { // 总和不变,返回(说明并发修改停止)
break;
}
last = sum;
if (retries > RETRIES_BEFORE_LOCK) { // 已加锁,直接返回
break;
}
}
if (overflow || sum > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
} else {
return (int)sum;
}
}
关键细节:
- 先尝试无锁累加(重试3次),若总和不变则返回;
- 若重试失败,对所有Segment加锁(全局锁),确保累加的准确性(此时并发性能极低,但
size
操作不是高频操作)。
3. JDK1.7的优缺点
- 优点:分段锁机制提高了并发度(并发度=Segment数量,默认16);
- 缺点:
- 结构复杂(Segment+HashEntry);
- 扩容成本高(Segment内部扩容,无法跨Segment协作);
- 链表查询效率低(长度过长时,查询时间复杂度为O(n))。
三、JDK1.8的ConcurrentHashMap:数组+链表+红黑树
JDK1.8彻底重构了ConcurrentHashMap
的结构,去掉了Segment,采用Node数组+链表+红黑树的结构,并用**CAS+synchronized
**替代了分段锁,进一步优化了并发性能。
1. 核心结构
java
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
transient volatile Node<K,V>[] table; // 哈希表(Node数组,默认容量16)
transient volatile Node<K,V>[] nextTable; // 扩容时的临时新表
transient volatile long baseCount; // 基础计数器(无竞争时使用)
transient volatile int sizeCtl; // 控制标志(-1:正在初始化;-N:有N个线程正在扩容;正数:下一次扩容的阈值)
// ...
}
// 普通Node节点(链表节点)
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修饰,保证链表结构的可见性
// ...
}
// 红黑树的根节点(包装TreeNode)
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root; // 红黑树的根
volatile TreeNode<K,V> first; // 链表的头节点(红黑树转为链表时使用)
volatile Thread waiter; // 等待锁的线程(用于computeIfAbsent等方法)
// ...
}
// 红黑树节点
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; // 是否为红节点
// ...
}
// 扩容标记节点(ForwardingNode):旧表中的节点转移到新表后,旧表的位置会设置为该节点
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable; // 新表
// ...
}
2. 核心属性解析
table
:主哈希表(Node数组),默认容量16,扩容时变为2倍;nextTable
:扩容时的临时新表(容量为旧表的2倍);sizeCtl
:控制哈希表的初始化和扩容:sizeCtl = -1
:正在初始化;sizeCtl = -N
:有N个线程正在扩容;sizeCtl > 0
:下一次扩容的阈值(table.length * loadFactor
,默认0.75);sizeCtl = 0
:未初始化(默认值);
baseCount
:无竞争时的元素计数器(用volatile
修饰);counterCells
:竞争时的元素计数器(数组,每个元素是CounterCell
,用volatile
修饰,减少竞争)。
3. 核心操作分析
(1)初始化:initTable()
ConcurrentHashMap
的初始化是懒加载的(第一次put
时触发),用CAS
保证线程安全。
java
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0) { // 正在初始化,让出CPU
Thread.yield();
} 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; // 默认容量16
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2); // 计算阈值(n * 0.75)
}
} finally {
sizeCtl = sc; // 设置阈值
}
break;
}
}
return tab;
}
关键细节:
CAS
设置sizeCtl
为-1,防止多个线程同时初始化;- 初始化后,
sizeCtl
设置为阈值(容量*0.75
)。
(2)put操作:putVal()
put
操作是ConcurrentHashMap
的核心,逻辑较复杂,需处理初始化、无锁插入、加锁插入、扩容协助、红黑树转换等场景。
java
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(); // 不允许null键/值
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(); // 初始化table
} else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 当前位置为空,用CAS插入新节点
if (casTabAt(tab, i, null, new Node<>(hash, key, value, null))) {
break; // 插入成功,退出循环
}
} else if ((fh = f.hash) == MOVED) { // 当前位置是ForwardingNode(正在扩容),帮助扩容
tab = helpTransfer(tab, f);
} else { // 当前位置有节点,加锁插入(synchronized锁链表头节点)
V oldVal = null;
synchronized (f) { // 锁链表头节点(或红黑树根节点)
if (tabAt(tab, i) == f) { // 再次检查(防止并发修改)
if (fh >= 0) { // 链表节点(hash>=0)
binCount = 1;
for (Node<K,V> e = f;; binCount++) {
K ek;
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 找到相同key
oldVal = e.val;
if (!onlyIfAbsent) {
e.val = value; // 修改值(volatile保证可见性)
}
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) { // 到达链表尾部,插入新节点
pred.next = new Node<>(hash, key, value, null);
break;
}
}
} else if (f instanceof TreeBin) { // 红黑树节点(hash<0,因为TreeBin的hash是-2)
Node<K,V> p;
binCount = 2;
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) { // 链表长度超过8,转红黑树
treeifyBin(tab, i);
}
if (oldVal != null) {
return oldVal; // 返回旧值
}
break;
}
}
}
addCount(1L, binCount); // 更新元素数量
return null;
}
关键细节:
- hash计算:用
spread
方法优化hash的分散性((h ^ (h >>> 16)) & 0x7fffffff
),减少冲突; - 无锁插入:当前位置为空时,用
CAS
插入新节点(casTabAt
方法,底层调用Unsafe
的compareAndSwapObject
); - 扩容协助:若当前位置是
ForwardingNode
(hash == MOVED
,MOVED=-1),则调用helpTransfer
方法帮助扩容(多线程协作扩容); - 加锁插入:当前位置有节点时,对链表头节点(或红黑树根节点)加
synchronized
锁,防止并发修改; - 红黑树转换:链表长度超过
TREEIFY_THRESHOLD
(8)时,调用treeifyBin
方法转红黑树(若table容量小于MIN_TREEIFY_CAPACITY
(64),则先扩容); - 更新数量:调用
addCount
方法更新元素数量(用baseCount
和counterCells
实现,减少竞争)。
(3)扩容操作:transfer()
(多线程协作)
扩容是ConcurrentHashMap
的难点,JDK1.8采用多线程协作扩容,提高扩容速度。扩容的触发条件是:size > sizeCtl
(元素数量超过阈值)。
java
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 计算每个线程处理的桶数(stride):CPU核心数越多,stride越大(最小16)
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) {
stride = MIN_TRANSFER_STRIDE; // MIN_TRANSFER_STRIDE=16
}
if (nextTab == null) { // 初始化新表(容量为旧表的2倍)
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<K,V> fwd = new ForwardingNode<>(nextTab); // 扩容标记节点(hash=MOVED)
boolean advance = true; // 是否需要推进转移索引
boolean finishing = false; // 是否完成扩容
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 = Math.max(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;
sizeCtl = (n << 1) - (n << 1 >>> 2); // 更新阈值(新容量*0.75)
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { // 减少正在扩容的线程数
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) { // 检查是否还有线程在扩容
return;
}
finishing = advance = true;
i = n; // 重新遍历旧表,检查是否有遗漏的节点
}
} else if ((f = tabAt(tab, i)) == null) { // 旧表的当前桶为空,设置为ForwardingNode
advance = casTabAt(tab, i, null, fwd);
} else if ((fh = f.hash) == MOVED) { // 旧表的当前桶已处理(ForwardingNode),跳过
advance = true;
} else { // 旧表的当前桶有节点,加锁转移(synchronized锁桶的头节点)
synchronized (f) {
if (tabAt(tab, i) == f) { // 再次检查(防止并发修改)
Node<K,V> ln, hn; // ln:低位链表(hash & n == 0);hn:高位链表(hash & n != 0)
if (fh >= 0) { // 链表节点
int runBit = fh & n; // 计算当前节点的高位bit(0或n)
Node<K,V> lastRun = f;
// 找到最后一个连续的runBit节点(优化转移效率)
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
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.key;
V pv = p.val;
if ((ph & n) == 0) {
ln = new Node<>(ph, pk, pv, ln);
} else {
hn = new Node<>(ph, pk, pv, hn);
}
}
// 将低位链表放入新表的i位置,高位链表放入i+n位置
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
// 旧表的i位置设置为ForwardingNode(标记已处理)
setTabAt(tab, i, fwd);
advance = true;
} else if (f instanceof TreeBin) { // 红黑树节点
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
// 将红黑树节点分为低位和高位链表
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
if ((h & n) == 0) {
if (loTail == null) {
lo = (TreeNode<K,V>)e;
} else {
loTail.next = (TreeNode<K,V>)e;
}
loTail = (TreeNode<K,V>)e;
lc++;
} else {
if (hiTail == null) {
hi = (TreeNode<K,V>)e;
} else {
hiTail.next = (TreeNode<K,V>)e;
}
hiTail = (TreeNode<K,V>)e;
hc++;
}
}
// 将低位链表转为红黑树或链表(若节点数<=6,转链表)
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (lc != 0) ? new TreeBin<>(lo) : null;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (hc != 0) ? new TreeBin<>(hi) : null;
// 将低位和高位节点放入新表
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
// 旧表的i位置设置为ForwardingNode
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
关键细节:
- 多线程协作:每个线程处理
stride
个桶(默认16),通过transferIndex
(转移索引)分配任务(从旧表的最后一个桶开始,往前处理); - 扩容标记:旧表中的桶处理完成后,设置为
ForwardingNode
(hash=MOVED
),其他线程访问该桶时,会发现是ForwardingNode
,从而调用helpTransfer
方法帮助扩容; - 节点转移:将旧表中的节点分为低位链表(
hash & 旧容量 == 0
)和高位链表(hash & 旧容量 != 0
),分别放入新表的i
和i+旧容量
位置; - 红黑树处理:若红黑树的节点数<=
UNTREEIFY_THRESHOLD
(6),则转为链表(减少红黑树的维护成本)。
(4)get操作:无锁
get
操作不需要加锁,因为Node
的value
和next
是volatile
的,保证了可见性。
java
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int hash = spread(key.hashCode()); // 计算hash
if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & hash)) != null) {
if ((eh = e.hash) == hash) { // 当前节点是目标节点(hash相等)
if ((ek = e.key) == key || (ek != null && key.equals(ek))) {
return e.val;
}
} else if (eh < 0) { // 当前节点是红黑树或ForwardingNode(hash<0)
return (p = e.find(hash, key)) != null ? p.val : null;
}
// 遍历链表,查找目标节点
while ((e = e.next) != null) {
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
return e.val;
}
}
}
return null;
}
关键细节:
- 快速查找:先检查当前桶的头节点是否是目标节点(hash相等且key相等);
- 红黑树查找:若当前节点是
TreeBin
(hash=-2
),调用find
方法遍历红黑树(时间复杂度O(log n)); - ForwardingNode处理:若当前节点是
ForwardingNode
(hash=-1
),则通过nextTable
(新表)查找目标节点(扩容时的处理)。
(5)size计算:sumCount()
JDK1.8的size
计算采用**baseCount
+counterCells
**的方式,减少竞争。
java
public int size() {
long n = sumCount();
return (n < 0) ? 0 : (n > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n;
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount; // 累加基础计数器
if (as != null) {
for (int i = 0; i < as.length; i++) { // 累加counterCells数组中的值
if ((a = as[i]) != null) {
sum += a.value;
}
}
}
return sum;
}
// CounterCell:竞争时的计数器(用volatile修饰)
static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
关键细节:
- 无竞争时:用
baseCount
(volatile
)记录元素数量(addCount
方法直接修改baseCount
); - 有竞争时:用
counterCells
数组(每个元素是CounterCell
)记录元素数量(addCount
方法通过CAS
修改counterCells
中的元素,减少竞争); - sumCount:累加
baseCount
和counterCells
中的值,得到总元素数量(弱一致性,因为并发修改时counterCells
可能变化,但size
操作不是高频操作)。
4. JDK1.8的优缺点
- 优点:
- 锁粒度更小(节点锁,而非Segment锁);
- 无锁操作(CAS)提高了并发效率;
- 红黑树优化了链表的查询效率(O(n)→O(log n));
- 多线程协作扩容提高了扩容速度;
- 结构更简单(去掉了Segment);
- 缺点:
synchronized
的性能虽然优化了(偏向锁、轻量级锁),但在极高并发下,仍可能比ReentrantLock略低(但实际场景中差异很小);- 红黑树的维护成本较高(插入、删除时需要调整结构)。
四、JDK1.7与JDK1.8的对比
维度 | JDK1.7 | JDK1.8 |
---|---|---|
结构 | Segment数组+HashEntry链表 | Node数组+链表+红黑树 |
锁机制 | 分段锁(ReentrantLock) | CAS+synchronized (节点锁) |
扩容 | Segment内部扩容(单线程) | 多线程协作扩容(全局扩容) |
查询效率 | 链表(O(n)) | 链表(O(n))/红黑树(O(log n)) |
并发度 | Segment数量(默认16) | 节点数量(理论上无限) |
null键/值 | 不允许 | 不允许 |
五、ConcurrentHashMap的适用场景
- 高并发缓存:比如缓存用户信息、商品信息等,需要频繁读取和写入;
- 计数器:比如统计接口调用次数、用户点击次数等,需要原子性的
increment
操作; - 并发集合:比如多线程处理任务时,存储中间结果。
六、注意事项
- 不允许null键/值:
ConcurrentHashMap
不允许null
键或null
值(否则抛出NullPointerException
),而HashMap
允许; - 弱一致性:迭代器(
Iterator
)是弱一致的,不会抛出ConcurrentModificationException
,但会反映迭代开始后的最新状态; - 扩容时的性能:扩容时,多线程协作会提高扩容速度,但仍会占用一定的CPU资源(避免在高并发场景下频繁扩容,建议初始化时设置合适的容量);
computeIfAbsent
方法:该方法用于原子性地计算并插入值(若key不存在),内部用ReservationNode
(占位节点)防止多个线程同时计算同一个key的值(避免重复计算)。
七、总结
ConcurrentHashMap
是Java并发包中最常用的并发集合,其设计核心是在保证线程安全的前提下,最大化并发效率。JDK1.8的重构(去掉Segment、用CAS+synchronized
、红黑树)进一步优化了性能,使其成为高并发场景下的首选。
理解ConcurrentHashMap
的关键是:
- 锁粒度的优化(从Segment到Node);
- 无锁操作的应用(CAS);
- 高效数据结构的使用(红黑树);
- 多线程协作的设计(扩容时协助转移节点)。
通过深入剖析其源码,可以更好地理解并发编程中的锁优化、无锁算法、数据结构设计等核心思想,为编写高效的并发程序打下基础。