ConcurrentHashMap 可以理解为 “智能分段储物柜” ,它通过精细的锁设计,让多线程存取数据时 既安全又高效,不像老旧的 HashTable
那样粗暴地锁整个柜子,而是每个小格子(或区域)独立上锁,允许多线程同时操作不同区域。
一、核心设计
1. JDK 1.7 的分段锁(Segment)
- 结构:整个哈希表分成多个 段(Segment) ,每个段相当于一个独立的小哈希表,有自己的锁。
- 并发度:默认16段,允许16个线程同时操作不同段。
- 示例:线程A操作段1,线程B操作段2 → 互不干扰。
- 缺点:段数固定,扩容不够灵活。
2. JDK 1.8 的 CAS + synchronized 优化
- 锁粒度更细:直接对每个数组桶(链表头或红黑树根节点)加锁。
- CAS 无锁化:插入元素时先尝试无锁操作(CAS),失败才加锁。
- 示例:两个线程同时操作不同桶 → 完全并行。
二、关键特性
1. 线程安全且高效
-
读操作无锁:通过
volatile
关键字保证可见性。 -
写操作局部锁:只锁当前操作的桶(链表或树节点)。
-
对比 HashTable:
// HashTable(全表锁,性能差) synchronized (this) { /* 操作整个表 */ } // ConcurrentHashMap(局部锁,性能高) synchronized (node) { /* 操作单个节点 */ }
2. 动态扩容
- 触发条件:元素数量超过阈值(容量 × 负载因子)。
- 并发扩容:线程插入时发现正在扩容,会协助迁移数据,加速完成。
3. 统计大小(size())
- 分片计数:通过
baseCount
和CounterCell[]
分散统计,避免竞争。 - 最终一致性:size() 的结果是近似值,适合高并发场景。
三、核心操作原理
1. 插入元素(put())
- 计算哈希:定位到具体桶的位置。
- 无锁尝试:若桶为空,用 CAS 插入新节点。
- 加锁处理:若桶非空(链表或树),用
synchronized
锁住头节点再操作。 - 树化检查:链表长度≥8,且数组容量≥64时,转红黑树。
2. 获取元素(get())
- 全程无锁:依赖
volatile
保证数据可见性,直接读取值。
四、适用场景
-
高并发缓存:如电商商品库存的实时扣减。
ConcurrentHashMap<String, Integer> stock = new ConcurrentHashMap<>(); stock.put("iPhone", 1000); // 线程安全扣减库存 stock.computeIfPresent("iPhone", (k, v) -> v > 0 ? v - 1 : 0);
-
实时计数器:统计网站访问量。
ConcurrentHashMap<String, Long> visitCount = new ConcurrentHashMap<>(); visitCount.merge("homepage", 1L, Long::sum); // 原子性累加
-
替代同步代码块:简化多线程共享数据的保护逻辑。
五、对比其他线程安全容器
容器 | 锁机制 | 性能 | 适用场景 |
---|---|---|---|
HashTable | 全表锁 | 低 | 遗留代码兼容 |
Collections.synchronizedMap | 全表锁 | 低 | 简单转换旧代码 |
ConcurrentHashMap | 桶级锁 + CAS | 高 | 高并发读写 |
六、注意事项
-
复合操作仍需同步:
// 错误示例:check-then-act 需额外同步 if (map.containsKey(key)) { map.put(key, value); // 非原子操作,可能被其他线程打断 } // 正确做法:使用原子方法 map.putIfAbsent(key, value);
-
避免无界扩容:初始容量和负载因子需合理设置,防止内存耗尽。
-
迭代器弱一致性:迭代时可能反映其他线程的修改,但不保证实时性。
七、总结
ConcurrentHashMap 是并发编程的瑞士军刀:
- 高并发:锁粒度细,CAS 减少竞争。
- 安全灵活:动态扩容、分片计数。
- 适用性广:缓存、计数器、实时数据处理。
使用口诀:
「并发哈希表,安全又高效
分段锁进化到桶锁,CAS 配合 synchronized
高并发下显神威,缓存计数最拿手!」