ConcurrentHashMap是Java并发包里最常用的线程安全映射实现。它不像Collections.synchronizedMap那样给整个表加锁,而是通过更细粒度的控制,在多线程读写时既保证数据一致性,又维持较高吞吐量。理解它的安全机制,对写高并发缓存、计数器、会话存储非常关键。

JDK 1.7中的分段锁设计
在JDK 1.7及更早版本中,ConcurrentHashMap内部由多个Segment组成,每个Segment本质是一个可重入锁(继承ReentrantLock)并持有一个HashEntry数组。整体结构相当于把一张大表分成若干子表,写操作只需要获取对应Segment的锁,不同Segment之间的读写互不阻塞。
这种设计降低了锁竞争概率。例如,线程A修改Segment 1,线程B修改Segment 2,两者可完全并行。但如果多个线程恰好哈希到同一个Segment,仍然会串行执行。下面是简化版的Segment结构示意:
// JDK 1.7简化示意,非完整源码
static final class Segment<K,V> extends ReentrantLock {
transient volatile HashEntry<K,V>[] table;
// put方法先尝试获取锁
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
if (!tryLock()) {
// 未获取到锁时可能自旋或阻塞
lock();
}
try {
// 实际写入逻辑
} finally {
unlock();
}
}
}
分段锁的缺点是默认并发度固定(如16个Segment),若业务哈希分布极不均匀,部分Segment会成为热点。此外,size()方法需要尝试不加锁统计,失败后再加锁,有一定开销。
JDK 1.8的Node数组与CAS加锁
JDK 1.8彻底废弃Segment,改为与HashMap类似的数组加链表加红黑树结构。线程安全依靠三个手段:volatile修饰的Node数组与节点值保证可见性;CAS操作处理空桶插入;对非空桶头节点使用synchronized块锁定,粒度缩小到单个哈希桶。
当发生哈希冲突且桶首个节点已存在时,ConcurrentHashMap会对该头节点对象加锁,再执行链表或红黑树的修改。由于不同桶的头节点不同,绝大多数写操作不会互相阻塞。以下代码展示putVal核心思路:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
// 省略部分变量
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 空桶用CAS插入
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { // 锁定桶头节点
if (tabAt(tab, i) == f) {
// 链表或树插入逻辑
}
}
}
}
return null;
}
相比分段锁,1.8方案在高并发下表现更好,因为锁的粒度随哈希分布自然分散。同时,扩容时采用多线程协同迁移(helpTransfer),加快大表重建速度。但需注意,synchronized虽经锁升级优化,若某桶链表过长且未转树,仍可能短暂停顿。
size统计与弱一致性迭代
ConcurrentHashMap的size()和mappingCount()不返回精确瞬态值,而是基于分桶计数器(CounterCell)累加的近似结果。这是因为在高并发下维持全局精确计数需要昂贵同步,而多数业务只关心量级。下面代码说明计数器使用:
// 累加各CounterCell与baseCount
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}
迭代器被设计为弱一致性:创建后,原表的增删改可能不会完全反映,但不会抛出ConcurrentModificationException。这对避免遍历时锁住全表很重要。若业务必须强一致快照,应考虑复制或加外部锁,而非依赖迭代器。
常见误用与注意事项
computeIfAbsent在1.8里若映射函数又调用同一表的其他compute方法,可能在特定递归场景造成死锁或阻塞,因为函数执行期间持有桶锁。因此映射函数应保持轻量,禁止在其中做远程调用或复杂递归写操作。
另外,不要用null作键或值,否则会抛NullPointerException。若要缓存“无值”状态,可用Optional或特殊哨兵对象。以下示例展示安全用法:
ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
// 正确:函数内仅做本地计算
String val = cache.computeIfAbsent("key", k -> "default-" + k.length());
// 错误示范:函数内再写同一map可能导致问题
// cache.computeIfAbsent("a", k -> cache.computeIfAbsent("b", x -> "bad"));
总之,ConcurrentHashMap通过细粒度锁、CAS与volatile语义,在安全和性能间取得平衡。选型时若需跨节点事务或精确顺序,它并不适用,应转向外部协调组件。
ConcurrentHashMapJava并发分段锁修改时间:2026-08-05 00:12:45