Java中的ConcurrentHashMap加锁机制详解
ConcurrentHashMap 是Java中高效的线程安全哈希表,其加锁机制经过多次优化,核心设计为 减小锁粒度 和 无锁化操作,以提升并发性能。以下是其加锁机制的详细解析:
一、Java 7及之前的分段锁(Segment)
-
分段设计:
- 将整个哈希表分为多个 段(Segment) ,每个段相当于独立的哈希表。
- 默认16个段,允许最多16个线程并发操作不同段,减少竞争。
-
锁机制:
- 每个段继承自
ReentrantLock,操作前需获取对应段的锁。 - 优点:段间操作互不干扰,提高并发度。
- 缺点:段数固定,扩容不够灵活。
- 每个段继承自
二、Java 8及之后的优化:CAS + synchronized
Java 8摒弃分段锁,采用更细粒度的 桶级别锁 和 CAS无锁操作,结合以下机制:
1. 无锁化操作(CAS)
-
适用场景:初始化、插入空桶、更新值等。
-
示例:插入新节点时,若桶为空,直接通过CAS设置头节点,无需加锁。
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) break; -
2. 细粒度锁(synchronized)
-
锁对象:每个桶的头节点(链表或红黑树根节点)。
- 操作非空桶时,使用
synchronized锁住头节点。 - 优点:仅锁当前桶,其他桶操作不受影响。
synchronized (f) { // 处理链表或红黑树 } - 操作非空桶时,使用
3. 扩容协作
-
多线程协同扩容:
- 当线程插入数据时发现正在扩容,会协助迁移数据。
- 通过
ForwardingNode标记迁移状态,避免重复操作。 - 优点:加快扩容速度,减少性能波动。
三、核心操作流程
1. 插入元素(put)
- 计算哈希定位到桶。
- 桶为空:尝试CAS插入新节点。
- 桶非空:锁住头节点,处理链表或红黑树。
- 检查是否需要扩容,触发迁移。
2. 获取元素(get)
- 全程无锁:依赖
volatile保证可见性,直接遍历链表或树。
3. 扩容(transfer)
- 分配新数组,容量翻倍。
- 多线程协作迁移旧数据,每个线程负责一段区间。
- 迁移完成,替换旧数组引用。
四、关键设计优势
- 锁粒度极细:仅锁单个桶,不同桶操作完全并行。
- CAS减少锁竞争:空桶插入等场景无需加锁。
- 并发扩容:多线程协同迁移数据,降低扩容延迟。
五、性能对比
| 实现方式 | 锁粒度 | 并发度 | 适用场景 |
|---|---|---|---|
| Hashtable | 全表锁 | 低(单线程操作) | 遗留代码兼容 |
| 分段锁(Java 7) | 段锁(默认16段) | 中(段间并发) | 中等并发 |
| CAS + synchronized(Java 8+) | 桶锁 | 高(桶间完全并发) | 高并发、大数据量 |
六、总结
- Java 7:通过 分段锁 提升并发,但段数固定,灵活性不足。
- Java 8+ :采用 桶锁 + CAS,锁粒度更细,支持动态扩容协作,性能显著提升。
- 适用场景:高并发读写(如缓存、计数器)、需线程安全且高效的数据存储。
核心口诀:
「ConcurrentHashMap 强,线程安全效率高
Java 7 分段锁,段间并发性能保
Java 8 更优化,CAS 加锁粒度小
桶锁配合协同扩,高并发下稳如锚!」