前言
首先,ConcurrentHashMap实现线程安全在不同的java版本中实现方式各有不同,首先,ConcurrentHashMap在Java的不同版本中有不同的实现,尤其是Java 7和Java 8之间变化较大。Java 7采用分段锁,而Java 8之后改用CAS和synchronized。用户可能想知道最新的实现方式,所以我应该重点讲Java 8之后的版本。
需要详细说明ConcurrentHashMap如何通过CAS操作和synchronized块来保证线程安全。CAS用于无竞争情况下的快速插入,而synchronized则在有竞争时锁定链表或红黑树的头节点。这样既提高了并发度,又避免了全局锁的性能问题。
另外,扩容机制也是一个关键点。ConcurrentHashMap支持多线程同时扩容,通过设置transferIndex和sizeCtl等变量协调各线程的任务分配。需要解释如何拆分任务,每个线程负责一部分桶的迁移,这样可以加快扩容速度,减少阻塞时间。
然后,可能需要提到get操作的线程安全性,因为它不需要加锁,而是通过volatile变量和数组引用的不可变性来保证可见性。这里需要强调Node的val和next字段的volatile修饰,确保读取时能获取最新值。
还有,初始化时的线程安全处理,比如使用sizeCtl变量和CAS操作来确保只有一个线程进行初始化,其他线程自旋等待。同样,在计算size时,可能通过baseCount和CounterCell数组来减少竞争,利用LongAdder的思想。
至于为什么ConcurrentHashMap在Java 8之后放弃分段锁,本文也会总结对比前后的性能差异。以及说明分段锁的危害,比如内存消耗和并发度限制,而新的实现方式更细粒度,每个桶独立处理,提高了并发性能。
此外,可能需要对比Hashtable和Collections.synchronizedMap,指出它们的全局锁机制效率低下,而ConcurrentHashMap的优势在于更高的并发度和更低的锁竞争。
最后,总结ConcurrentHashMap如何通过多种机制协同工作,确保线程安全的同时保持高性能。可能还需要提醒用户在实际使用中注意正确使用,比如避免在迭代过程中修改结构,或者结合业务场景选择合适的并发容器。
综上所述,ConcurrentHashMap 是 Java 并发编程中线程安全的哈希表实现,其线程安全机制在 Java 7 和 Java 8+ 中有显著差异。以下以 Java 8+ 的实现为核心,详解其线程安全的设计原理。
一、核心设计思想
1.分段锁优化:
- Java 7:采用分段锁(Segment),每个段独立加锁,允许不同段并发操作。
- Java 8+:摒弃分段锁,改用 CAS + synchronized 锁单个桶(Node),锁粒度更细,并发度更高。
2.无锁化操作:
- 读操作全程无锁(依赖 volatile 可见性)。
- 写操作优先尝试无锁(CAS),冲突时使用 synchronized 锁。
二、数据结构
- 数组 + 链表/红黑树(类似 HashMap):
transient volatile Node<K,V>[] table;
- 关键节点:
- Node:链表节点,val 和 next 字段为 volatile,保证可见性。
- TreeBin:红黑树根节点,持有读写锁,确保树结构调整的线程安全。
三、线程安全实现细节
- 插入操作(put)
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ...
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化数组(CAS控制)
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 桶为空时,使用 CAS 插入新节点
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
} else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 协助扩容
else {
synchronized (f) { // 锁住桶头节点
if (tabAt(tab, i) == f) {
// 链表或红黑树插入
if (fh >= 0) { /* 链表处理 */ }
else if (f instanceof TreeBin) { /* 红黑树处理 */ }
}
}
}
}
// ...
}
- CAS 无锁插入:当桶为空时,直接通过 casTabAt(底层为 sun.misc.Unsafe 的 CAS 操作)插入新节点。
- synchronized 锁头节点:当桶非空时,锁住头节点,防止其他线程并发修改链表或树。
- 扩容机制(transfer)
- 多线程协同扩容:
1.触发扩容的线程初始化 nextTable(新数组),并设置 transferIndex 为旧数组长度。
2.每个线程领取一段连续的桶区间(如从 transferIndex 向前分配),迁移该区间内的数据。
3.迁移完成后,通过 ForwardingNode 标记旧桶为已迁移。 - 关键变量:
- sizeCtl:控制扩容状态(如负数表示正在扩容)。
- transferIndex:分配迁移任务的指针。
- 读操作(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());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 遍历链表或红黑树
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0) // 红黑树查找
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
// ...
}
}
return null;
}
- 无锁读取:依赖 volatile 的 val 和 next 字段保证可见性。
- 红黑树查询:通过 TreeBin 的读写锁实现并发安全。
- 计数器(size)
-
baseCount + CounterCell[]:
类似 LongAdder 的分段计数设计,减少竞争。- baseCount:基础计数值(CAS 更新)。
- CounterCell[]:当竞争激烈时,分散更新到多个 CounterCell 中。
-
最终 size:总和为 baseCount + sum(CounterCell)。
四、关键线程安全机制
- 内存可见性
- volatile 变量:table 数组、节点的 val 和 next 字段均用 volatile 修饰,确保多线程间的可见性。
- 内存屏障:通过 Unsafe 的 putOrderedObject 等方法避免指令重排序。
- CAS 操作
- 原子性保障:
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
```
用于初始化数组、插入新节点、更新计数器等场景。
- 锁优化
- 细粒度锁:仅锁单个桶的头节点,不同桶的操作可并行。
- 红黑树锁:TreeBin 内部使用读写锁,允许并发读,写操作。
五、对比其他线程安全 Map
六、总结
ConcurrentHashMap 的线程安全通过以下机制实现:
1.CAS 无锁化:无竞争时快速更新,减少锁开销。
2.细粒度锁:仅锁单个桶,允许高并发操作。
3.多线程协同扩容:分散迁移任务,避免阻塞。
4.内存可见性:volatile 变量和内存屏障保证数据一致性。
5.分段计数:减少计数器竞争。
这些设计使其在高并发场景下性能显著优于传统的同步容器(如 Hashtable),成为 Java 并发编程中的核心工具。