引言
在Java并发编程中,数据结构的线程安全性是至关重要的。当涉及到多线程环境下的键值对存储时,`java.util.concurrent.ConcurrentHashMap`(简称CHM)扮演了无可替代的角色。它巧妙地结合了HashMap的高效性和Hashtable的线程安全性,从而成为高并发场景下首选的数据结构之一。本文将深入剖析ConcurrentHashMap的设计原理、关键特性,并通过实例演示其在实际开发中的应用。
一、分段锁(Segment Locking)
在Java早期版本(JDK 1.7及之前)中,java.util.concurrent.ConcurrentHashMap使用了分段锁机制来实现高效的并发访问。其核心思想是将整个哈希表划分为多个独立的段(Segments),每个段都可以独立地加锁和解锁,从而实现了更细粒度的并发控制。
以下是一个简化的示例,帮助理解分段锁的基本概念:
// 注意:以下代码仅用于演示分段锁的思想,并非真实的ConcurrentHashMap源码
import java.util.concurrent.locks.ReentrantLock;
public class SegmentLockExample {
// 假设Segment类代表ConcurrentHashMap中的一个分段
static class Segment<K, V> {
// 每个Segment内部包含一个类似HashMap的数据结构
HashEntry<K, V>[] table;
// 分段锁
final ReentrantLock lock = new ReentrantLock();
// 插入操作,需要先获取segment对应的锁
void put(K key, V value) {
lock.lock();
try {
// 省略具体插入逻辑...
int hash = key.hashCode();
// 找到对应位置并插入数据
// ...
} finally {
lock.unlock();
}
}
// 其他操作如get、remove等同样需要通过lock保护
}
// Segment数组模拟ConcurrentHashMap中的所有分段
static class ConcurrentHashMap<K, V> {
final Segment<K, V>[] segments;
ConcurrentHashMap(int concurrencyLevel, int initialCapacity) {
// 初始化segments数组大小,根据并发级别计算
segments = new Segment[concurrencyLevel];
// 省略其他初始化逻辑...
}
// 对外提供的put方法,需要定位到对应的Segment进行操作
public V put(K key, V value) {
int hash = key.hashCode();
int segmentIndex = hash >>> segmentShift; // 计算key所属的Segment索引
Segment<K, V> segment = segments[segmentIndex];
segment.put(key, value);
return null; // 返回值仅为示意
}
}
// 模拟Segment内部存储节点的HashEntry类
static class HashEntry<K, V> {
K key;
V value;
HashEntry<K, V> next;
HashEntry(K key, V value, HashEntry<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
}
}
二、链地址法结合CAS(Compare and Swap)
在Java 8及更高版本的java.util.concurrent.ConcurrentHashMap中,虽然摒弃了分段锁(Segment Locking)的设计,但仍然使用链地址法来解决哈希冲突,并结合CAS(Compare and Swap)操作实现无锁化的并发控制。以下是一个简化的示例以说明其基本原理:
// 注意:以下代码仅用于演示ConcurrentHashMap中的CAS和链地址法的思想,并非真实的源码
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicReferenceArray;
public class ConcurrentHashMapCasExample {
static final Unsafe unsafe;
// 假设Node类是ConcurrentHashMap内部的一个节点结构
static class Node<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;
}
}
// 使用AtomicReferenceArray模拟ConcurrentHashMap的数组部分
static final class Table<K, V> extends AtomicReferenceArray<Node<K, V>> {
Table(int length) {
super(length);
}
}
// 初始化Unsafe实例,用于CAS操作
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private final Table table; // 实际的ConcurrentHashMap会动态调整table大小
ConcurrentHashMap() {
table = new Table<>(initialCapacity); // 初始化表的大小
}
// 使用CAS进行put操作(简化版)
public V put(K key, V value) {
int hash = spread(key.hashCode());
int index = hash & (table.length() - 1); // 计算桶索引
for (Node<K, V> e = table.get(index); e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
// 如果找到了键值对,则使用CAS更新value
while (!unsafe.compareAndSwapObject(e, Node.valOffset, e.val, value)) {
// 当前值被其他线程修改,循环重试直到成功
}
return e.val; // 返回旧值
}
}
// 添加新节点到链表头部
Node<K, V> newNode = new Node<>(hash, key, value, table.get(index));
while (!table.compareAndSet(index, null, newNode)) { // 空桶直接添加
Node<K, V> oldNode = table.get(index);
newNode.next = oldNode; // 已有节点则插入链表头部
if (table.compareAndSet(index, oldNode, newNode)) {
return null; // 成功插入返回null
}
}
return null; // 插入新节点返回null
}
// 静态方法获取对象field偏移量,实际ConcurrentHashMap内部会有类似的方法
private static long valOffset;
static {
try {
valOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("val"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
// 扩散函数,简化版,实际ConcurrentHashMap内部有更复杂的处理
private static int spread(int h) {
return (h ^ (h >>> 16)) & Integer.MAX_VALUE;
}
}
上述示例展示了如何利用CAS操作在链表上安全地更新或插入元素。实际上,在真正的ConcurrentHashMap中,为了进一步提高性能,还引入了红黑树等数据结构,当链表长度达到阈值时会转化为红黑树以优化查询效率。同时,扩容、迁移等操作也都是通过CAS原子操作保证线程安全的。
三、红黑树转换
在Java 8及更高版本的java.util.concurrent.ConcurrentHashMap中,当链表长度超过一定阈值时,会将链表转换为红黑树以优化查询性能。这个过程是由内部类TreeNode(继承自Node)和相关方法实现的。以下是一个简化的示例来说明红黑树转换的基本原理:
// 注意:以下代码是基于ConcurrentHashMap实际逻辑简化模拟
import java.util.concurrent.atomic.AtomicReferenceArray;
public class ConcurrentHashMapRBTreeExample {
// 假设Node是基础节点类,而TreeNode是用于构建红黑树的节点类
static class Node<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;
}
}
static class TreeNode<K, V> extends Node<K, V> {
TreeNode<K, V> parent; // 红黑树中的父节点引用
TreeNode<K, V> left, right; // 左右子树引用
boolean red; // 是否是红色节点(红黑树特性)
TreeNode(int hash, K key, V val, Node<K, V> next) {
super(hash, key, val, null);
this.left = this.right = null;
this.red = true; // 新创建的节点默认为红色
}
// 省略了红黑树相关的旋转、着色等平衡操作...
}
private final AtomicReferenceArray<Node> table; // 实际ConcurrentHashMap使用此数组存储数据
private static final int TREEIFY_THRESHOLD = 8; // 转换为红黑树的阈值
public V put(K key, V value) {
// ...省略定位到桶的操作...
// 当链表过长时,进行树化
if (e instanceof TreeNode) { // 如果已经是树节点,则直接插入
TreeNode<K, V> p = (TreeNode<K, V>) e;
TreeNode<K, V> newNode = new TreeNode<>(hash, key, value, null);
TreeNode<K, V> r = tryInsertTreeNode(root, p, newNode); // 插入新节点并保持红黑树性质
return r == null ? null : r.val;
} else if (count >= TREEIFY_THRESHOLD) { // 链表长度达到阈值,尝试树化
TreeNode<K, V> hd = treeifyBin(tab, i); // 将链表转换为红黑树
setTabAt(tab, i, hd); // 更新桶的头节点为红黑树根节点
}
// ...省略后续更新节点value和其他并发控制逻辑...
}
// 尝试将一个链表bin转换成红黑树
private TreeNode<K, V> treeifyBin(Node<K, V>[] tab, int index) {
Node<K, V> b; // 指向当前桶的第一个元素
if ((tab != null) && (tab.length > MIN_TREEIFY_CAPACITY)
&& (b = tabAt(tab, index)) != null
&& b.hash != MOVED) { // 判断是否满足树化条件
// 省略了实际树化的过程,包括将链表转换为红黑树的具体算法...
return new TreeNode<>(...); // 返回新构建的红黑树根节点
} else {
// 不满足树化条件则进行扩容或其他处理
// ...
}
}
// 简化版的红黑树插入方法,实际ConcurrentHashMap内部有更复杂的平衡调整操作
private TreeNode<K, V> tryInsertTreeNode(TreeNode<K, V> root, TreeNode<K, V> parent, TreeNode<K, V> newNode) {
// 省略了插入新节点后的红黑树旋转和重新着色等平衡操作...
return newNode; // 示例中仅返回新插入的节点
}
}
实际的ConcurrentHashMap中,红黑树的插入、删除、查找以及旋转、着色等操作都实现了严格遵循红黑树特性的复杂逻辑,这里为了简化起见,并没有展示这些细节。但上述代码足以表达出ConcurrentHashMap中链表转红黑树的大致流程
四、JDK 1.8后的改进
在Java 1.8及更高版本中,java.util.concurrent.ConcurrentHashMap进行了显著的优化和改进。以下主要从数据结构、并发控制以及操作方法等方面进行讲解,并提供简化版的代码示例。
数据结构改进
- 摒弃Segment(分段锁)设计:在JDK 1.7之前,ConcurrentHashMap使用了Segment数组配合HashEntry链表实现分段锁,而在JDK 1.8中,放弃了这种设计,改为直接使用Node对象数组+链表/红黑树的数据结构,并且通过CAS和volatile关键字保证并发访问的安全性。
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
volatile V val; // 使用volatile保证可见性
volatile Node<K, V> next;
// ...省略构造函数和其他内部方法...
}
transient volatile Node<K,V>[] table;
- 引入红黑树转换:当链表长度超过8时,会将链表转化为红黑树以提高查找效率。同时,当红黑树节点数量低于6时,又会将其退化为链表。
并发控制改进
- 无锁化:利用CAS(Compare And Swap)操作来避免使用传统的互斥锁,从而提升并发性能。
- 扩容策略:在扩容过程中采用了一种更加高效的并发迁移算法,多个线程可以并行地帮助完成扩容任务,极大地减少了因扩容导致的阻塞时间。
操作方法改进
- putVal()方法:插入键值对的核心方法,它包含了各种并发控制逻辑,如初始化桶、处理哈希冲突、执行扩容等,并且在必要时会进行链表转红黑树的操作。
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;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K, V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// ...此处省略了大量的并发控制和节点插入、更新逻辑...
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
// ...判断是否需要替换节点或添加新节点...
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); // 转换为红黑树
}
}
}
}
}
}
五、扩容机制
Java 1.8版本的java.util.concurrent.ConcurrentHashMap扩容机制在保证线程安全的前提下,采用了更为高效的并发扩容方式。当容器中的元素数量超过负载因子所设定的阈值时,
ConcurrentHashMap会自动进行扩容。扩容的具体步骤包括:
1.初始化与扩容触发条件:初始容量通过构造函数设置,如果未指定,则使用默认值,并且容量始终是2的幂次方。当实际存储的节点数(包括链表和树节点)达到容量乘以负载因子的大小时,将触发扩容。
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// Find a power of 2 >= initialCapacity
int capacity = roundUpToPowerOf2(initialCapacity);
this.loadFactor = loadFactor;
table = new Node[capacity];
threshold = (int)(capacity * loadFactor); // 设置阈值
}
// 辅助方法用于向上取整到最近的2的幂
static final int roundUpToPowerOf2(int number) {
return Integer.highestOneBit((number - 1) << 1);
}
2.扩容过程:扩容过程中,新的容量通常为旧容量的两倍。扩容不是一次性完成,而是采用了一种并行迁移的方式。当一个线程发现需要扩容时,它首先标记当前桶(Node数组的一个位置)的状态为MOVED,然后创建一个新的、更大的数组,并开始迁移原数组中的元素到新数组中。其他线程在执行put操作时,如果遇到MOVED状态的桶也会参与到扩容迁移的过程中。
以下是简化版的扩容逻辑示例:
// 假设transfer方法是处理扩容的核心逻辑
final Node<K,V>[] transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// ...省略部分代码...
while (s-- > 0) {
Node<K,V> f; int n, i, fh;
if ((f = tabAt(tab, i)) != null && (fh = f.hash) >= 0) {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= UNTREEIFY_THRESHOLD) { // 树节点转链表
TreeNode<K,V> ft = (TreeNode<K,V>)f;
TreeNode<K,V> t = splitTreeNode(ft, nextTab, i, false);
ln = t.lo;
hn = t.hi;
} else {
// 链表节点直接拆分
ArrayList<Node<K,V>> al = new ArrayList<>(2);
al.add(f);
for (Node<K,V> e = f.next; e != null; e = e.next)
al.add(e);
int alSize = al.size();
ln = null;
hn = null;
for (int j = 0; j < alSize; j++) {
Node<K,V> e = al.get(j);
int h = e.hash;
int k = h & (nextTab.length - 1);
if ((ln == null) || (h < 0))
ln = new Node<K,V>(h, e.key, e.val, null);
else
setTabAt(nextTab, k, e);
}
}
setTabAt(nextTab, i, ln);
setTabAt(tab, i, fwd); // 将老桶置为转发节点
// 同理处理高位节点hn
}
}
}
}
return nextTab;
}
// 调用扩容的方法
void resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
int newCap = oldCap << 1;
Node<K,V>[] newTab = new Node[newCap];
//...省略转移数据的部分...
table = newTab;
threshold = (int)(newCap * loadFactor);
}
六、线程安全的迭代器
Java的ConcurrentHashMap提供了线程安全的迭代器Iterator,可以在多线程环境下安全地遍历ConcurrentHashMap中的元素。与HashMap不同,ConcurrentHashMap的迭代器不会抛出ConcurrentModificationException异常,即使在迭代过程中有其他线程修改了ConcurrentHashMap的内容。
下面是一个使用ConcurrentHashMap的线程安全迭代器的示例:
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 创建一个ConcurrentHashMap
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 添加一些元素
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
// 获取一个迭代器
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
// 遍历ConcurrentHashMap
while(iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
}
}
在上面的示例中,我们首先创建了一个ConcurrentHashMap对象,并向其中添加了一些元素。然后,我们通过调用entrySet()方法获取了ConcurrentHashMap中的所有键值对的集合,并通过调用iterator()方法获取了一个迭代器对象。最后,我们使用该迭代器对象遍历了ConcurrentHashMap中的所有元素。
需要注意的是,虽然ConcurrentHashMap的迭代器是线程安全的,但是它只能保证在遍历过程中不会出现ConcurrentModificationException异常,而不能保证迭代器返回的元素不会被其他线程修改或删除。因此,在多线程环境下,如果需要保证迭代器返回的元素不被修改或删除,需要使用额外的同步机制来保护ConcurrentHashMap中的元素。
总结
ConcurrentHashMap以其独特的设计理念和精巧的实现方式,成功解决了多线程环境下的数据共享问题。了解并掌握ConcurrentHashMap的工作原理及其在并发编程中的最佳实践,对于提高Java应用程序的性能和稳定性具有重要意义。开发者应当根据实际需求选择合适的API方法,充分利用ConcurrentHashMap所提供的并发优势。