在Java并发编程中,ConcurrentHashMap是一个非常重要的数据结构,它提供了一种线程安全的哈希表实现。随着Java版本的迭代,ConcurrentHashMap的实现也在不断优化,以更好地支持高并发场景。本文将深入探讨ConcurrentHashMap的底层存储结构、红黑树转换时机、核心属性sizeCtl、散列算法、计数器的安全机制以及size方法的实现策略。通过对这些功能点的详细分析,我们将揭示ConcurrentHashMap如何在高并发环境下保持高效性和线程安全性。
在Java 8及以后的版本中,ConcurrentHashMap采用了数组、链表和红黑树的组合结构。默认情况下,ConcurrentHashMap会初始化一个长度为16的数组,数组的每个元素都是一个链表或红黑树的头节点。这种设计旨在平衡查询效率和空间占用。
数组是ConcurrentHashMap存储哈希表的基本结构。通过哈希函数,键被映射到数组的一个索引上。如果多个键的哈希值相同(即发生了哈希冲突),它们将被存储在同一个链表或红黑树上。
链表用于解决哈希冲突。当多个元素哈希值相同时,它们会被存储在同一个链表上。链表的插入和删除操作的时间复杂度为O(n),其中n为链表的长度。
红黑树是一种自平衡的二叉搜索树,用于在链表长度过长时提高查询效率。当链表长度超过8且数组长度大于64时,链表会转换成红黑树。红黑树的插入、删除和查找操作的时间复杂度为O(logn),其中n为树中节点的数量。
以下是ConcurrentHashMap初始化数组和Node节点的部分代码:
java复制代码
// 初始化数组
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
elseif (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
// 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(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
// 省略其他方法...
}
当链表长度超过8且数组长度大于64时,链表会转换成红黑树。这一转换旨在优化查询性能,避免因链表过长导致的查询慢问题。
如果链表长度超过8但数组长度小于64,则先进行数组扩容操作(数组长度变为原来的二倍),然后再考虑是否将链表转换为红黑树。
以下是链表转红黑树的部分代码:
java复制代码
// treeifyBin方法尝试将链表转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1); // 先扩容
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val, null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
链表转红黑树的优化主要体现在以下两个方面:
sizeCtl是ConcurrentHashMap中的一个核心属性,用于控制初始化和扩容操作。它在扩容过程中表示当前扩容的状态,并在初始化时表示默认的初始容量。
sizeCtl的值域分为以下几种情况:
在初始化过程中,sizeCtl的值用于控制并发初始化的线程安全。当多个线程尝试同时初始化数组时,只有一个线程能够成功将sizeCtl的值从默认值修改为-1,并获得初始化数组的权限。其他线程则通过自旋等待初始化完成。
在扩容过程中,sizeCtl的值用于表示当前扩容的状态和进度。扩容操作会创建一个新的数组,并将旧数组中的元素迁移到新数组中。sizeCtl的高16位存储扩容版本号,用于区分连续的多次扩容;低16位表示当前参与扩容的线程数量。
以下是sizeCtl在初始化和扩容过程中的部分代码实现:
java复制代码
// 初始化数组时修改sizeCtl的值
private final Node<K,V>[] initTable() {
// ... 省略部分代码 ...
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// ... 省略部分代码 ...
sizeCtl = n - (n >>> 2); // 设置下次扩容的阈值
} finally {
// ... 省略部分代码 ...
}
}
// ... 省略部分代码 ...
}
// 扩容时修改sizeCtl的值
private final void addCount(long x, int check) {
// ... 省略部分代码 ...
if (check >= 0) {
// ... 省略部分代码 ...
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
// ... 省略并发控制代码 ...
} else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2)) {
// 进行扩容
transfer(tab, null);
// ... 省略部分代码 ...
}
}
}
// ... 省略部分代码 ...
}
散列算法是一种将任意长度的消息压缩到一个固定长度的输出的算法。在ConcurrentHashMap中,散列算法用于将键映射到一个固定的桶中。
ConcurrentHashMap使用的散列算法主要包括以下步骤:
ConcurrentHashMap中的散列算法通过以下方式进行了优化:
以下是ConcurrentHashMap中散列算法的部分代码实现:
java复制代码
// 计算哈希值的spread方法
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
// 基于key计算索引位置
int i = (n - 1) & spread(key.hashCode());
ConcurrentHashMap使用计数器来跟踪元素的数量。在并发环境下,计数器的更新操作需要保证线程安全。
为了保证计数器的线程安全,ConcurrentHashMap采用了以下安全机制:
以下是ConcurrentHashMap中计数器更新和读取的部分代码实现:
java复制代码
// 更新计数器
private final void addCount(long x, int check) {
// ... 省略部分代码 ...
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// ... 省略并发控制代码 ...
}
}
// 读取计数器
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
ConcurrentHashMap的size()方法用于返回当前映射中键值对的数量。由于ConcurrentHashMap设计为线程安全且高效,其size()方法在实现上需要考虑并发环境下的性能和一致性。
ConcurrentHashMap中size()方法的实现通常包含以下步骤:
以下是ConcurrentHashMap中size()方法的简化版代码实现:
java复制代码
public int size() {
long sum = 0;
for (Segment<K,V> segment : segments) {
sum += segment.count;
}
return (int)sum;
}
需要注意的是,上述代码是简化版实现,实际实现可能更加复杂,并涉及到并发控制等细节。
为了提高size()方法的性能,ConcurrentHashMap采用了以下优化措施:
本文深入探讨了ConcurrentHashMap的底层存储结构、红黑树转换时机、核心属性sizeCtl、散列算法、计数器的安全机制以及size方法的实现策略。通过对这些功能点的详细分析,我们揭示了ConcurrentHashMap如何在高并发环境下保持高效性和线程安全性。
随着Java版本的迭代和硬件技术的发展,ConcurrentHashMap的实现也将不断优化。未来,我们可以期待ConcurrentHashMap在以下几个方面取得进展:
通过持续的优化和创新,ConcurrentHashMap将继续在Java并发编程中发挥重要作用。