Java并发容器:toBeBetterJavaer ConcurrentHashMap详解
一、ConcurrentHashMap概述
ConcurrentHashMap是Java并发编程中常用的线程安全哈希表实现,它在保证高并发读写性能的同时,提供了比Hashtable更优的并发访问控制机制。作为HashMap的线程安全版本,ConcurrentHashMap在多线程环境下被广泛应用于缓存、会话存储等场景。
1.1 核心特性
- 分段锁机制:JDK 7采用分段锁(Segment)提高并发度,JDK 8改用CAS+synchronized实现更细粒度的锁控制
- 高并发性:支持多线程同时读写,读操作几乎无锁竞争
- 迭代安全性:支持并发修改的迭代器,不会抛出ConcurrentModificationException
- 原子操作支持:提供putIfAbsent、remove、replace等原子性操作方法
1.2 与其他并发容器对比
| 容器类型 | 锁机制 | 读性能 | 写性能 | 适用场景 |
|---|---|---|---|---|
| Hashtable | 全表锁 | 低 | 低 | 简单并发场景 |
| Collections.synchronizedMap | 全表锁 | 低 | 低 | 简单并发场景 |
| ConcurrentHashMap | 分段锁/CAS | 高 | 高 | 高并发读写场景 |
官方文档:Java并发编程基础
二、底层实现原理
2.1 JDK 7实现:分段锁机制
JDK 7中ConcurrentHashMap采用"分段锁"设计,将整个哈希表分为多个Segment(默认16个),每个Segment相当于一个小的HashMap。
// JDK 7核心结构
public class ConcurrentHashMap<K, V> {
// 内部Segment数组,每个Segment都是一个线程安全的HashMap
final Segment<K, V>[] segments;
// 每个Segment的默认大小
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 默认并发级别,决定了Segment的数量
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
}
// Segment内部类,继承自ReentrantLock
static final class Segment<K, V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K, V>[] table;
transient int count;
}
这种设计使得不同Segment上的操作可以并行进行,理论上最高支持16个线程同时写入(等于Segment数量)。
2.2 JDK 8实现:CAS+synchronized
JDK 8彻底重构了ConcurrentHashMap的实现,取消了Segment分段锁,转而采用CAS操作和synchronized关键字实现更细粒度的同步控制。
核心改进点:
- 用Node数组替代Segment数组
- 对链表头节点使用synchronized加锁
- 利用CAS操作实现无锁修改
- 引入红黑树优化长链表查询性能(同HashMap)
数据结构示意图:
┌───────────────────────────────────────────────────────┐
│ Node[] table │
├───────────┬───────────┬───────────┬───────────┬───────┤
│ Node │ Node │ Node │ Node │ ... │
│ ┌───────┐ │ ┌───────┐ │ ┌───────┐ │ ┌───────┐ │ │
│ │HashEntry│ │ │HashEntry│ │ │TreeNode │ │ │HashEntry│ │ │
│ └───────┘ │ └───────┘ │ └───────┘ │ └───────┘ │ │
│ │ │ │ │ │ │ │ │ │
│ ┌───────┐ │ ┌───────┐ │ ┌───────┐ │ ┌───────┐ │ │
│ │HashEntry│ │ │HashEntry│ │ │TreeNode │ │ │HashEntry│ │ │
│ └───────┘ │ └───────┘ │ └───────┘ │ └───────┘ │ │
└───────────┴───────────┴───────────┴───────────┴───────┘
JDK 8中Node节点定义:
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
volatile V val;
volatile Node<K, V> next;
// ...
}
三、核心操作实现
3.1 初始化机制
ConcurrentHashMap的初始化通过懒加载方式实现,首次put操作时才真正创建数组。
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K, V>[] tab = table; ; ) {
Node<K, V> f;
int n, i, fh;
// 初始化table(懒加载)
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// ...
}
}
3.2 put操作流程
- 计算key的哈希值
- 检查table是否初始化,未初始化则进行初始化
- 根据哈希值找到对应的数组索引
- 如果该位置为空,使用CAS操作插入新节点
- 如果该位置为MOVED(扩容中),帮助扩容
- 否则对该位置的头节点加synchronized锁
- 遍历链表或红黑树,执行插入或替换操作
- 检查是否需要将链表转换为红黑树
- 更新元素计数,检查是否需要扩容
源码参考:Java集合框架解析
3.3 扩容机制
ConcurrentHashMap的扩容操作是并发进行的,当元素数量达到阈值(容量*负载因子)时触发。
JDK 8中的扩容优化:
- 支持多线程同时参与扩容
- 扩容过程中仍可进行读写操作
- 采用高低位拆分转移节点,减少锁竞争
private final void transfer(Node<K, V>[] tab, Node<K, V>[] nextTab) {
int n = tab.length, stride;
// 根据CPU核心数计算每个线程处理的桶数量
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// ...
}
四、常见问题解析
4.1 为什么ConcurrentHashMap比Hashtable效率高?
Hashtable使用synchronized修饰方法,导致每次读写都需要获取整个对象锁,并发性能较差。而ConcurrentHashMap在JDK 7通过分段锁,JDK 8通过CAS和细粒度synchronized锁,允许多个线程同时访问不同的桶,大大提高了并发吞吐量。
4.2 ConcurrentHashMap的迭代器是强一致性还是弱一致性?
ConcurrentHashMap的迭代器是弱一致性的,它不保证反映迭代过程中所有的实时修改,但也不会抛出ConcurrentModificationException异常。这与HashMap的快速失败迭代器形成对比。
4.3 为什么key和value不能为null?
ConcurrentHashMap不允许key或value为null,而HashMap允许。这是因为在并发环境下,无法区分null是正常的值还是表示键不存在,可能导致歧义。
// ConcurrentHashMap的get方法
public V get(Object key) {
Node<K, V>[] tab;
Node<K, V> e, p;
int n, eh;
K ek;
int h = spread(key.hashCode()); // key为null会抛出NullPointerException
// ...
}
相关教程:Java并发编程面试题
五、最佳实践
5.1 适用场景
- 高并发环境下的缓存实现
- 多线程共享数据存储
- 会话管理
- 计数器实现
5.2 使用注意事项
- 初始化容量设置:根据预期数据量合理设置初始容量,避免频繁扩容
- 并发级别选择:JDK 7需要指定合适的并发级别,JDK 8已自动优化
- 避免长时间持有锁:尽量避免在迭代过程中执行耗时操作
- 优先使用原子操作:充分利用putIfAbsent、remove、replace等原子方法
// 原子操作示例:实现简易缓存
ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
// 避免双重检查锁定的最佳实践
public Object getCache(String key) {
Object value = cache.get(key);
if (value == null) {
value = cache.putIfAbsent(key, new Object());
if (value == null) {
value = loadFromDatabase(key);
}
}
return value;
}
5.3 性能调优建议
- 合理设置初始容量和负载因子(默认0.75)
- 避免使用size()方法(需要遍历整个数组,性能较差)
- 多线程环境下优先使用批量操作方法(putAll、forEach等)
- 对于读多写少场景,考虑使用ReadWriteLock结合HashMap
六、总结
ConcurrentHashMap作为Java并发编程中的重要容器,通过不断优化的并发控制机制,在保证线程安全的同时提供了出色的性能。从JDK 7的分段锁到JDK 8的CAS+synchronized,其实现原理的演进反映了并发编程技术的发展趋势。
掌握ConcurrentHashMap的内部实现和使用技巧,对于编写高效的多线程程序至关重要。在实际开发中,应根据具体场景选择合适的并发容器,并遵循最佳实践以获得最优性能。
进阶学习:Java并发编程实战
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



