ConcurrentHashMap
能在高并发环境中保持良好性能,核心原因之一就是其**“细粒度加锁 + 无锁化读操作”**的并发控制策略。它在不同版本中使用了不同的加锁手段:
✅ 一、JDK 1.7:Segment 分段锁(Segment Locking)
🌟 结构:Segment[] + HashEntry[]
-
ConcurrentHashMap
被分为多个 Segment(默认 16 段) -
每个 Segment 内部维护一个独立的 Hash 表和
ReentrantLock
-
多个线程可以并行访问不同 Segment,提高并发度
🧠 加锁策略:
segment.lock(); // 对当前段加独占锁
try {
// put 操作
} finally {
segment.unlock();
}
🚫 局限性:
-
Segment 是粗粒度锁:每段仍只能一个线程操作,写热点集中时性能下降
-
结构复杂,维护成本高
✅ 二、JDK 1.8:CAS + synchronized(锁粒度更细)
🌟 结构:Node<K, V>[] table
+ 链表/红黑树
,不再使用 Segment
-
整体类似
HashMap
,但对并发进行了以下优化:
✅ 1. 插入时:使用 CAS + synchronized
步骤(以 putVal()
为例):
-
无冲突插入(无锁):
-
目标桶为空 → 使用
CAS
原子地设置新节点
if (tabAt(table, i) == null) { if (casTabAt(table, i, null, new Node(...))) { return; } }
-
-
有冲突时:桶中已有节点 → 进入同步代码块
synchronized (bin) { // 链表插入 or 树节点插入 }
✅ 同步锁粒度控制在某个 bucket(bin)级别,而不是整张表或 Segment。
✅ 2. 读操作:大多数是 无锁的
-
读取时直接使用
volatile
保证可见性,避免加锁开销 -
Node
的val
和next
都是volatile
,读线程看到的都是一致数据 -
特殊情况下(如正在 resize)会短暂阻塞
✅ 3. 扩容时:协作式转移(协同 resize)
-
多个线程可以共同参与扩容操作,分担迁移任务
-
避免了单线程 resize 带来的长时间阻塞
✅ 三、小结:如何实现高效加锁?
技术点 | 说明 |
---|---|
分段锁(JDK1.7) | 每个 Segment 独立加锁,允许并发写不同段,减小锁竞争 |
CAS + synchronized(JDK1.8) | 空桶用 CAS 插入,无锁,冲突桶只锁当前 bin,锁粒度极细 |
读操作无锁化 | 大部分读取完全无锁,仅靠 volatile 保证可见性,极大提升读取性能 |
协作 resize | 多线程共同扩容,避免长时间阻塞,提高扩容吞吐 |
✅ 四:实战建议
-
不要手动对
ConcurrentHashMap
加锁(如自己再包一层synchronized
),会降低并发性能 -
尽量使用其提供的原子方法,如
putIfAbsent()
、computeIfAbsent()
来代替复合操作 -
高并发缓存场景首选
ConcurrentHashMap
如你希望深入解析 JDK8 中具体的 putVal()
、resize()
、treeifyBin()
等源码细节,我可以逐行带你阅读并讲解内部机制。是否继续?