文章目录
ConcurrentHashMap
是 Java 并发包 (java.util.concurrent
) 中的一个线程安全的哈希表实现,专为高并发场景设计。它通过 分段锁(Java 7)或 CAS + synchronized(Java 8 及以后)机制,显著提升了并发性能,同时支持动态扩容、链表转红黑树等优化。以下从多个维度详细介绍其核心特性、底层实现及使用方法。
一、ConcurrentHashMap的核心特性
1. 线程安全
- 所有操作(如
put
、get
、remove
)在多线程环境下保证线程安全。 - 与
Hashtable
和synchronizedMap
的区别:Hashtable
和synchronizedMap
对整个 Map 加锁,导致并发性能差。ConcurrentHashMap
使用 分段锁(Java 8 之前)或 CAS + synchronized(Java 8 及以后),锁粒度更细,允许多个线程同时操作不同部分。
2. 高并发性能
- Java 8 之前:使用分段锁(
Segment
),每个段独立加锁,减少锁竞争。 - Java 8 及以后:采用 CAS 操作 + synchronized 锁,进一步降低锁开销,提升吞吐量。
- 弱一致性迭代器:迭代器不会抛出
ConcurrentModificationException
,但可能反映部分更新。
3. 支持原子操作
- 提供
putIfAbsent
、remove
、replace
等原子方法,避免手动加锁。 - Java 8+ 新增
computeIfAbsent
、forEach
等方法,简化并发编程。
4. 动态扩容
- 支持自动扩容,当元素数量超过阈值时重新分配数组,无需阻塞所有线程。
二、ConcurrentHashMap的底层实现
1. Java 7 的实现(分段锁)
数据结构
- Segment 数组:Segment[] segments,每个 Segment 是一个独立的锁和小型哈希表。每个 Segment 内部维护一个 HashEntry[] 数组,每个 HashEntry 节点存储键值对,并通过链表解决哈希冲突。
- 锁粒度:每个段独立加锁,修改操作仅锁定相关段,其他段可并发访问。
- 缺点:分段锁在并发度极高时仍存在锁竞争问题。
核心机制
- Segment 分段:默认 16 个段,每个段对应一部分桶(Bucket)。
- HashEntry 不可变:
HashEntry
的key
、hash
、next
为final
,value
为volatile
,保证读操作无需加锁。
代码示例(简化版)
public class ConcurrentHashMap<K, V> {
// Segment 数组,分段锁的核心结构
final Segment<K, V>[] segments;
// Segment 内部类(Java 7)
static final class Segment<K, V> extends ReentrantLock {
// 哈希桶数组
volatile HashEntry<K, V>[] table;
// 元素数量
transient int count;
// 修改次数
transient int modCount;
// 扩容阈值
transient int threshold;
// 负载因子
final float loadFactor;
// HashEntry 节点
static final class HashEntry<K, V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K, V> next;
HashEntry(int hash, K key, V value, HashEntry<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
}
}
2. Java 8 及以后的实现(CAS + synchronized)
数据结构
- 数组 + 链表/红黑树:底层结构与
HashMap
类似,但通过 CAS 操作和synchronized
锁控制并发。 - 锁粒度:对链表的头节点或红黑树的根节点加
synchronized
锁,锁粒度更细。
核心优化
- CAS 操作:用于无锁化初始化/扩容和插入空桶。
- 链表转红黑树:当链表长度超过阈值(默认 8)且数组长度大于 64 时,链表转换为红黑树。
- 并发扩容:使用
ForwardingNode
逐步迁移数据,避免全局锁。
示例代码
// Node 节点(Java 8)
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
}
// 红黑树节点(Java 8)
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
三、ConcurrentHashMap的核心方法
方法 | 描述 |
---|---|
V put(K key, V value) | 插入键值对,若键已存在则替换。 |
V get(Object key) | 获取指定键的值。 |
V remove(Object key) | 移除指定键的值。 |
boolean containsKey(Object key) | 判断是否包含指定键。 |
void clear() | 清空所有键值对。 |
V putIfAbsent(K key, V value) | 如果键不存在,则插入值。 |
boolean remove(Object key, Object value) | 如果键对应的值等于指定值,则移除。 |
boolean replace(K key, V oldValue, V newValue) | 如果键的旧值等于指定值,则替换为新值。 |
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) | 如果键不存在或值为 null ,则计算并插入新值。 |
void forEach(BiConsumer<? super K, ? extends V> action) | 遍历所有键值对,执行操作。 |
示例代码:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("Apple", 10); // 插入键值对
map.putIfAbsent("Banana", 5); // 如果键不存在则插入
map.computeIfAbsent("Cherry", k -> 15); // 计算并插入新值
System.out.println(map.get("Apple")); // 输出 10
四、ConcurrentHashMap与Hashtable/Collections.synchronizedMap的对比
特性 | ConcurrentHashMap | Hashtable | Collections.synchronizedMap |
---|---|---|---|
线程安全 | ✅ 是 | ✅ 是 | ✅ 是 |
锁粒度 | 分段锁(Java 8 前) / CAS + synchronized(Java 8+) | 全局锁 | 全局锁 |
并发性能 | 高 | 低 | 低 |
迭代器一致性 | 弱一致性 | 快速失败 | 快速失败 |
原子操作 | ✅ 支持(如 putIfAbsent ) | ❌ 不支持 | ❌ 不支持 |
适用场景 | 高并发读写 | 低并发场景 | 低并发场景 |
五、ConcurrentHashMap的使用场景
1. 高频读写的缓存
- 适用于缓存系统(如 Redis 客户端本地缓存),需要高效并发访问和原子更新。
2. 分布式计数器
- 多线程环境下统计计数(如页面访问量),避免手动加锁。
3. 任务分组
- 在生产者-消费者模型中,按任务类型分组存储和处理任务。
4. 配置管理
- 动态加载和更新配置项,支持并发读取和局部更新。
六、ConcurrentHashMap的注意事项
1. 弱一致性迭代器
- 迭代器不会抛出
ConcurrentModificationException
,但可能反映部分更新(如新增的元素可能未被遍历到)。
2. 避免死锁
- 使用
computeIfAbsent
时,注意避免递归调用导致死锁(例如在映射函数中再次调用put
)。
3. 内存一致性
- 写操作对其他线程的可见性通过
volatile
和 CAS 保证,符合 Java 内存模型规范。
七、ConcurrentHashMap的参考代码
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, AtomicInteger> counterMap = new ConcurrentHashMap<>();
// 多线程并发操作
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
String key = "item" + (i % 10);
counterMap.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出结果
counterMap.forEach((key, value) -> {
System.out.println(key + ": " + value);
});
}
}
八、总结
ConcurrentHashMap
是 Java 中高性能线程安全 Map 的首选实现,尤其适合高并发读写场景。其核心优势在于通过分段锁(Java 8 前)或 CAS + synchronized(Java 8+)机制,显著降低了锁竞争,提升了吞吐量。与 Hashtable
和 synchronizedMap
相比,它在性能和功能上更具优势。理解其内部实现和适用场景,有助于在并发编程中高效使用这一工具。