上一篇介绍了ConcurrentHashMap的核心特性,本文来分析ConcurrentHashMap的核心数据结构和API方法。
核心数据结构
ConcurrentHashMap的核心数据结构是:数组(Node[] table)+链表+红黑树的形式,而这核心数据结构主要由一个基础父类节点和四个子类节点构成。
基础节点Node
java
public class ConcurrentHashMap<K, V> {
// 哈希表数组,存储Node节点
transient volatile Node<K, V>[] table;
// 内部类: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;
// Node的构造函数等...
}
}
Node节点是哈希表结构的基础存储单元,作为其他四种节点类型的父类。Node是普通的Entry结点,以链表形式存储实际数据。其定义包含以下关键字段:
final int hash:键的哈希值
final K key:不可变的键
volatile V val:使用volatile修饰的值,保证可见性
volatile Node<K,V> next:链表指针,同样使用volatile修饰
当发生哈希冲突时,新节点会以链表形式连接在桶位的头节点之后。
红黑树容器TreeBin和节点TreeNode
java
// 内部类:TreeBin红黑树节点容器
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;//红黑树根节点
volatile TreeNode<K,V> first;//双向链表头节点(first)
volatile Thread waiter;
volatile int lockState;
...
}
// 内部类:TreeNode节点,继承自Node,用于红黑树结构
static final class TreeNode<K, V> extends Node<K, V> {
TreeNode<K, V> parent; // 父节点
TreeNode<K, V> left; // 左子节点
TreeNode<K, V> right; // 右子节点
boolean red; // 节点颜色(红或黑)
// TreeNode的构造函数及红黑树相关方法等...
}
TreeBin和TreeNode是ConcurrentHashMap中处理红黑树结构的关键组件,它们之间是容器与节点的关系。
TreeBin作为红黑树的容器节点,内部持有指向TreeNode双向链表头节点(first)和红黑树根节点(root)的引用。
TreeNode则代表红黑树中的单个节点,同样继承自Node类,但额外包含红黑树所需的parent、left、right等指针以及颜色标记red。
这种设计实现了TreeBin对红黑树的结构管理和并发控制,而TreeNode专注于存储数据和维护树结构。
java
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2, and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
将链表结构转换为红黑树结构的桶节点数阈值。当向一个桶中添加元素时,如果该桶中的节点数达到或超过此值,就会将链表转换为红黑树。此值必须大于2,并且为了与树结构在收缩时转换回普通桶的假设相协调,该值至少应为8。此时桶位中存储的是TreeBin对象而非TreeNode对象。
扩容节点ForwardingNode
java
// 内部类:ForwardingNode节点,用于在扩容时转发请求到新的table
static final class ForwardingNode<K, V> extends Node<K, V> {
final Node<K, V>[] nextTable; // 指向新的哈希表数组
// ForwardingNode的构造函数等...
}
在ConcurrentHashMap的扩容过程中,ForwardingNode节点发挥着关键的协调和转发作用。当某个哈希桶内的所有节点都完成数据迁移到新数组(nextTable)后,就会在旧数组的对应桶位放入ForwardingNode节点作为标记。
这个节点本身不存储实际的数据键值对,而是作为一个转发器。当其他线程在查询时遇到ForwardingNode,查询操作会被引导到扩容后的新数组上进行。如果有线程执行put操作时发现当前桶位已是ForwardingNode,该线程会调用helpTransfer()方法协助进行扩容操作,待扩容完成后再将新节点放入新数组的对应位置。作为内部机制的一部分来协调并发操作。
临时节点ReservationNode
java
// 内部类:computeIfAbsent and compute时使用
static final class ReservationNode<K,V> extends Node<K,V> {
ReservationNode() {
super(RESERVED, null, null);
}
Node<K,V> find(int h, Object k) {
return null;
}
}
ReservationNode是ConcurrentHashMap中的一种特殊占位符节点,主要在compute()和computeIfAbsent()方法中使用。该节点在正式赋值之前起到暂时占位的作用,和TreeBin、ForwardingNode节点一样,不存储实际的数据键值对。
当执行compute或computeIfAbsent操作时,如果目标桶位为空,ConcurrentHashMap会先创建一个ReservationNode作为临时占位符。这个机制确保了在复杂的函数式计算过程中,其他线程不会同时修改同一个桶位,从而保证了操作的原子性和线程安全性。在计算完成后,这个ReservationNode占位符会被替换为包含实际数据的普通Node节点。作为内部机制的一部分来协调并发操作。
核心方法
ConcurrentHashMap的节点类型构成了其并发架构的基础,核心API方法通过这些精心设计的节点体系实现高效的并发控制,主要包括基础数据操作、状态查询和特殊功能三大类控制。
核心操作方法
put() / putVal()方法
这是最核心的插入方法,采用CAS+synchronized实现线程安全。当目标桶位为空时,使用CAS操作直接插入新节点;当桶位不为空时,对桶位的头节点加synchronized锁,然后在链表或红黑树中执行插入操作。如果插入后链表长度达到TREEIFY_THRESHOLD(默认8)且数组容量≥64,链表会转换为红黑树结构。
java
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 参数校验:ConcurrentHashMap不允许key或value为null
if (key == null || value == null) throw new NullPointerException();
// 计算key的哈希值,通过spread方法分散哈希碰撞
int hash = spread(key.hashCode());
int binCount = 0; // 记录链表长度或树节点数
// 无限循环,确保在各种情况下都能完成插入操作
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
// 情况1:哈希表未初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化哈希表
// 情况2:目标桶位为空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 使用CAS无锁操作插入新节点
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // 插入成功则退出循环
// 情况3:当前桶位正在扩容迁移
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 协助扩容
// 情况4:检查首节点是否匹配且onlyIfAbsent为true
else if (onlyIfAbsent // 不加锁检查首节点
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv; // 直接返回现有值,不进行插入
// 情况5:需要处理哈希冲突
else {
V oldVal = null;
// 对桶位头节点加锁,保证线程安全
synchronized (f) {
// 双重检查:确认加锁后头节点未被修改
if (tabAt(tab, i) == f) {
// 子情况5.1:处理链表结构
if (fh >= 0) {
binCount = 1; // 链表节点计数从1开始
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 查找是否已存在相同key的节点
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val; // 保存旧值
if (!onlyIfAbsent)
e.val = value; // 覆盖现有值
break;
}
Node<K,V> pred = e;
// 到达链表尾部,插入新节点
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
// 子情况5.2:处理红黑树结构
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2; // 树节点计数设为2
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value; // 更新树节点值
}
}
// 子情况5.3:处理ReservationNode(递归更新检测)
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
// 后处理:检查是否需要树化或返回旧值
if (binCount != 0) {
// 链表长度达到阈值时进行树化
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); // 链表转红黑树
// 如果覆盖了现有值,返回旧值
if (oldVal != null)
return oldVal;
break; // 插入成功,退出循环
}
}
}
// 更新元素计数,采用分片计数减少竞争
addCount(1L, binCount);
return null; // 新插入节点返回null
}
CAS插入节点方法
java
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSetReference(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
第一个参数表示需要进行原子更新的哈希表对象。
第二个参数表示目标字段在对象中的内存偏移量。
第三个参数表示期望的旧值,传入的是null。
第四个参数表示要设置的新值,也就是新插入的节点。
方法执行流程总结
1.参数校验:确保key和value都不为null
2.哈希计算:通过spread方法优化哈希分布
3.表初始化:延迟初始化哈希表数组
4.空桶处理:使用CAS无锁插入
5.扩容协助:发现正在扩容时协助迁移
6.快速检查:在onlyIfAbsent模式下不加锁检查首节点
7.哈希冲突处理:对头节点加锁后处理链表或树
8.数据结构转换:必要时进行链表到红黑树的转换
9.计数更新:采用分片计数机制
get()方法
get方法通过volatile读实现无锁查询。首先根据key的hash值定位到数组索引,然后遍历该桶位的链表或红黑树查找匹配的节点。由于Node的val和next字段都使用volatile修饰,保证了多线程环境下的内存可见性。
java
public V get(Object key) {
// 定义局部变量:tab-哈希表数组,e-当前节点,p-临时节点,n-数组长度,eh-节点哈希值,ek-节点键值
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算key的哈希值,通过spread方法分散哈希冲突
int h = spread(key.hashCode());
// 步骤1:基础条件检查
// 检查哈希表已初始化、数组长度有效,并且目标桶位不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 步骤2:检查首节点是否匹配
// 比较哈希值,如果哈希值匹配再比较key
if ((eh = e.hash) == h) {
// 通过引用相等或equals方法比较key
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val; // 匹配成功,返回节点值
}
// 步骤3:处理特殊节点(哈希值为负)
// 当eh < 0时,表示当前节点是特殊节点:TreeBin(哈希值-2)、ForwardingNode(哈希值-1)或ReservationNode(哈希值-3)
else if (eh < 0)
// 调用对应节点的find方法进行查找
// - TreeBin:在红黑树中查找
// - ForwardingNode:转发到新表中查找
// - ReservationNode:返回null
return (p = e.find(h, key)) != null ? p.val : null;
// 步骤4:遍历链表查找
// 如果首节点不匹配且不是特殊节点,则遍历链表
while ((e = e.next) != null) {
// 对每个节点进行哈希值和key的匹配检查
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val; // 找到匹配节点,返回值
}
}
// 步骤5:未找到匹配的key,返回null
return null;
}
方法执行流程总结
1.哈希计算阶段
2.首节点快速检查(这种设计优化了最常见的命中情况)
3.特殊节点处理
4.链表遍历阶段
remove()方法
删除操作同样采用synchronized锁住桶位头节点的方式保证线程安全。在删除节点后,如果树结构的节点数减少到UNTREEIFY_THRESHOLD(默认6)以下,红黑树会退化为链表。
状态查询方法
size() / mappingCount()方法
size()方法返回int类型的元素个数,而mappingCount()返回long类型,更推荐使用后者以避免整数溢出。
isEmpty()方法
通过遍历数组检查是否存在非空桶位来实现,由于不需要精确计数,性能比size()方法更高。
原子更新方法
compute() / computeIfAbsent() / computeIfPresent()方法
这些方法支持原子性的函数式更新。在执行过程中,如果目标桶位为空,会先创建ReservationNode作为占位符,待计算完成后替换为实际节点。这种机制确保了在复杂的函数式计算过程中,其他线程不会同时修改同一个桶位。
merge()方法
合并操作也采用类似的并发控制机制,当原值不存在时直接插入新值,存在时通过BiFunction合并原值和新值。
扩容相关方法
transfer()方法
负责数据迁移的核心扩容方法,采用多线程并发迁移策略。每个线程负责迁移数组的一个分片,迁移完成后在旧桶位中放入ForwardingNode节点标记该桶位已迁移完成。
helpTransfer()方法
当其他线程在操作过程中发现当前桶位是ForwardingNode时,会调用此方法协助进行数据迁移。
三种Map的并发性能对比
下面通过一个例子来对比ConcurrentHashMap、HashMap、HashTable的并发性能,这个例子采用10个线程并发往map中添加数据,每个线程添加10万次。观察三种Map的并发添加耗时。
java
public class MapConcurrentTest {
private static final int THREAD_COUNT = 10;
private static final int OPERATION_COUNT = 100000;
public static void main(String[] args) throws InterruptedException {
// 测试三种Map的并发性能
testHashMap();
testHashTable();
testConcurrentHashMap();
}
// 测试HashMap(非线程安全)
public static void testHashMap() throws InterruptedException {
Map<String, Integer> map = new HashMap<>();
System.out.println("=== HashMap 测试 ===");
long startTime = System.currentTimeMillis();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < OPERATION_COUNT; j++) {
String key = "key-" + threadId + "-" + j;
map.put(key, j);
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
System.out.println("HashMap 最终大小: " + map.size() +
" (期望: " + (THREAD_COUNT * OPERATION_COUNT) + ")");
System.out.println("HashMap 耗时: " + (endTime - startTime) + "ms");
System.out.println();
}
// 测试HashTable(线程安全但性能较低)
public static void testHashTable() throws InterruptedException {
Map<String, Integer> map = new Hashtable<>();
System.out.println("=== HashTable 测试 ===");
long startTime = System.currentTimeMillis();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < OPERATION_COUNT; j++) {
String key = "key-" + threadId + "-" + j;
map.put(key, j);
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
System.out.println("HashTable 最终大小: " + map.size() +
" (期望: " + (THREAD_COUNT * OPERATION_COUNT) + ")");
System.out.println("HashTable 耗时: " + (endTime - startTime) + "ms");
System.out.println();
}
// 测试ConcurrentHashMap(高性能线程安全)
public static void testConcurrentHashMap() throws InterruptedException {
Map<String, Integer> map = new ConcurrentHashMap<>();
System.out.println("=== ConcurrentHashMap 测试 ===");
long startTime = System.currentTimeMillis();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < OPERATION_COUNT; j++) {
String key = "key-" + threadId + "-" + j;
map.put(key, j);
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
System.out.println("ConcurrentHashMap 最终大小: " + map.size() +
" (期望: " + (THREAD_COUNT * OPERATION_COUNT) + ")");
System.out.println("ConcurrentHashMap 耗时: " + (endTime - startTime) + "ms");
System.out.println();
}
}
运行结果:
=== HashMap 测试 ===
HashMap 最终大小: 768182 (期望: 1000000)
HashMap 耗时: 277ms
=== HashTable 测试 ===
HashTable 最终大小: 1000000 (期望: 1000000)
HashTable 耗时: 385ms
=== ConcurrentHashMap 测试 ===
ConcurrentHashMap 最终大小: 1000000 (期望: 1000000)
ConcurrentHashMap 耗时: 323ms
从运行结果可以看出,HashMap的最终大小小于期望值,可能出现数据丢失,这证实了其非线程安全的特性。HashTable的运行结果数据一致性得到保证,但执行时间明显长于其他两种实现,这反映了其全局锁机制带来的性能开销。ConcurrentHashMap的运行结果数据一致性得到保证,性能优于HashTable。