# 问题
7. ConcurrentHashMap 采用了哪些锁优化机制?为什么比 Hashtable 高效?
# 标准答案
ConcurrentHashMap 采用 CAS(无锁操作)+ Synchronized(局部锁)+ Volatile(可见性)+ 红黑树(减少冲突) 的综合锁优化机制,实现更高效的并发控制。相较于 Hashtable 使用的全局 Synchronized 锁,ConcurrentHashMap 通过更细粒度的并发控制 降低锁竞争,大幅提升读写性能。
# 答案解析
# 1. 为什么 Hashtable 效率低?
Hashtable 的所有方法(put() / get() / remove()
等)都使用了 synchronized
关键字,导致:
- 全局锁竞争严重:所有线程竞争同一把锁,读写都串行化,读写互斥,影响吞吐量。
- 锁粒度大,阻塞严重:即使不同 key 存在不同的哈希桶,访问仍然需要获取全局锁,不能并发操作多个桶。
- 扩容时卡顿严重:整个
Hashtable
扩容时需要重新计算所有 key 的 hashCode(),并且是单线程执行,影响性能。
示例:多线程 put()
操作会被全局锁同步,导致高并发下性能下降:
public synchronized V put(K key, V value) {
int hash = key.hashCode();
...
}
2
3
4
# 2. ConcurrentHashMap 的锁优化机制
# (1)CAS(无锁操作)降低竞争
在 put()
操作中,ConcurrentHashMap 先尝试使用 CAS(compareAndSwap)无锁操作 更新数据,避免加锁。
- 若目标桶(bin)为空,则 CAS 直接插入新节点。
- 若有哈希冲突,则 synchronized 仅锁定该 bin,减少锁竞争。
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, val, null))) {
// 通过 CAS 操作无锁插入
}
2
3
优势:多数情况下,无需加锁即可更新数据,提高并发性能。
# (2)Synchronized 局部锁(代替全局锁)
如果 CAS 失败(即发生哈希冲突),ConcurrentHashMap 只对该哈希桶(bin)加锁,而非整个 Map,加速并发写入。
synchronized (f) {
// 只在哈希冲突时锁定该 bin
}
2
3
优势:只影响冲突的 Key,不影响其他 Key 的并发写入,锁粒度更小,并发度更高。
# (3)Volatile 保证可见性,避免锁开销
Node.val
和table
采用 volatile 修饰,确保修改后对其他线程可见,避免synchronized
额外的锁开销。
static class Node<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
2
3
4
5
6
优势:读操作可以无锁完成,提高吞吐量。
# (4)红黑树减少高并发下的哈希冲突
- 当链表长度超过 8 时,链表转换为红黑树,查询时间复杂度 O(n) → O(log n)。
- 避免哈希冲突退化为链表遍历,提高写入和查找效率。
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
2
优势:降低哈希冲突,提升查询和插入性能。
# (5)并行扩容,避免全局锁竞争
JDK 1.8 采用分段扩容,允许多个线程并发迁移不同的桶,提升扩容效率。
- 旧版 Hashtable 需要全局锁扩容,所有操作暂停,影响吞吐量。
- ConcurrentHashMap 通过
ForwardingNode
让线程知道桶已迁移,避免竞争。
Node<K,V> f;
if ((f = tabAt(nextTab, i)) == null)
casTabAt(nextTab, i, new Node<K,V>(fh, null, null, null));
2
3
优势:多个线程同时扩容,提升效率,避免全局阻塞。
# 3. 并发性能对比(ConcurrentHashMap vs Hashtable)
特性 | Hashtable(JDK 1.7) | ConcurrentHashMap(JDK 1.8) |
---|---|---|
加锁机制 | 全局 synchronized | CAS + Synchronized 局部锁 |
锁粒度 | 整个 Map | 仅锁定哈希桶 |
读性能 | 受锁影响,低 | 读操作基本无锁,高 |
写性能 | 线程争抢全局锁,低 | CAS + 局部锁,高 |
扩容 | 全局锁,扩容卡顿 | 并发扩容,吞吐量高 |
高并发表现 | 线程阻塞严重 | 低冲突,高吞吐量 |
# 4. ConcurrentHashMap 的常见问题
❌ 常见误区
误以为 ConcurrentHashMap 读写都无锁
- 实际上,读操作基本无锁,但写操作(哈希冲突)时仍然会对某个 bin 加
synchronized
。
- 实际上,读操作基本无锁,但写操作(哈希冲突)时仍然会对某个 bin 加
误认为 JDK 1.8 仍然使用 Segment
- JDK 1.8 完全去除了
Segment
,直接使用Node[]
结构,配合CAS + synchronized
实现高并发。
- JDK 1.8 完全去除了
忽视扩容带来的性能波动
- 并发扩容虽然减少了全局锁竞争,但迁移数据仍然有成本,可能影响短时间内的性能。
# 深入追问
🔹 为什么 ConcurrentHashMap 选择 synchronized
而不是 ReentrantLock
?
🔹 为什么红黑树转换阈值是 8,而不是更小?
🔹 putIfAbsent()
方法在高并发环境下是否一定安全?
🔹 并行扩容时如何避免数据丢失?
# 相关面试题
• ConcurrentHashMap 的并发优化机制?
• 为什么 ConcurrentHashMap 读操作比 Hashtable 快?
• ConcurrentHashMap 在高并发场景下的性能优化点?