在 Java 的并发编程中,ConcurrentHashMap
是一个非常重要的数据结构。它位于 java.util.concurrent
包中,提供了线程安全的哈希表实现,能够在多线程环境下高效地进行读写操作。本文将深入探讨 ConcurrentHashMap
的内部实现、线程安全机制以及在不同 JDK 版本中的变化。
1 ConcurrentHashMap
的线程安全机制
ConcurrentHashMap
的设计初衷是为了解决 HashMap
在多线程环境下扩容时可能导致的 CPU 占用接近 100% 的问题。HashMap
本身并不是线程安全的,虽然可以通过 Collections.synchronizedMap(Map<K,V> m)
将其包装成线程安全的 Map
,但这种方式在高并发场景下性能较差。
ConcurrentHashMap
通过锁分段技术(JDK 1.7)或 CAS 操作(JDK 1.8)来实现线程安全,大大提高了并发性能。
2 JDK 1.7 中的 ConcurrentHashMap
在 JDK 1.7 中,ConcurrentHashMap
采用了一种称为分段锁(Lock Striping)的机制。这种机制将整个哈希表分成多个段(Segment),每个段都独立加锁。读取操作不需要锁,写入操作仅锁定相关的段。这种设计减少了锁冲突的几率,从而提高了并发性能。
2.1 分段锁的优势
- 高并发吞吐量:在并发环境下,分段锁机制能够实现更高的吞吐量。
- 低性能损失:在单线程环境下,分段锁机制只损失非常小的性能。
2.2 分段锁的实现
- Segment 数组:
ConcurrentHashMap
包含一个Segment
数组,每个Segment
类似于一个小的HashMap
。 - HashEntry 数组:每个
Segment
包含一个HashEntry
数组,用于存储键值对数据。 - 锁机制:每个
Segment
持有一把可重入锁(ReentrantLock
),当对HashEntry
数组的数据进行修改时,必须首先获得对应的Segment
锁。
2.3 分段锁的结构
ConcurrentHashMap
的结构可以看作是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表(即 Segment
)。每个 Segment
独立加锁,从而实现并发操作。
2.4 读写过程
-
get 方法:
- 为输入的 Key 做 Hash 运算,得到 hash 值。
- 通过 hash 值,定位到对应的
Segment
对象。 - 再次通过 hash 值,定位到
Segment
当中数组的具体位置。
-
put 方法:
- 为输入的 Key 做 Hash 运算,得到 hash 值。
- 通过 hash 值,定位到对应的
Segment
对象。 - 获取可重入锁。
- 再次通过 hash 值,定位到
Segment
当中数组的具体位置。 - 插入或覆盖
HashEntry
对象。 - 释放锁。
3 JDK 1.8 中的 ConcurrentHashMap
在 JDK 1.8 中,ConcurrentHashMap
进行了重大改进,主要体现在以下几个方面:
3.1 放弃分段锁
JDK 1.8 放弃了分段锁机制,转而使用 CAS 操作和 synchronized
关键字来保证并发安全性。整个容器只分为一个 Segment
,即 table
数组。
3.2 链表转红黑树
同 HashMap
一样,当链表长度达到 8 时,链表会转换为红黑树,以提高大量冲突时的查询效率。
3.3 CAS 操作
以某个位置的头结点(链表的头结点或红黑树的 root 结点)为锁,配合自旋+ CAS 避免不必要的锁开销,进一步提升并发性能。
3.4 节点类
JDK 1.8 中的 ConcurrentHashMap
对节点类进行了优化,使用 volatile
关键字保证多线程操作时变量的可见性。
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;
}
}
4 ConcurrentHashMap
的内部实现
4.1 字段
ConcurrentHashMap
中有几个关键字段:
- table:存放
Node
的数组,采用懒加载方式,直到第一次插入数据时才会初始化。 - nextTable:扩容时使用,平时为
null
。 - sizeCtl:控制
table
数组的大小,根据是否初始化和是否正在扩容有不同的含义。- 当值为负数时: 如果为
-1
表示正在初始化,如果为-N
则表示当前正有N-1
个线程进行扩容操作; - 当值为正数时: 如果当前数组为
null
的话表示table
在初始化过程中,sizeCtl
表示为需要新建数组的长度;若已经初始化了,表示当前数据容器(table 数组)可用容量,也可以理解成临界值(插入节点数超过了该临界值就需要扩容),具体指为数组的长度n
乘以 加载因子loadFacto
r; - 当值为
0
时,即数组长度为默认初始值。
- 当值为负数时: 如果为
- sun.misc.Unsafe U:用于实现 CAS 操作,保证线程安全。
CAS 操作依赖于现代处理器指令集,通过底层的CMPXCHG指令实现。CAS(V,O,N)核心思想为:若当前变量实际值 V 与期望的旧值 O 相同,则表明该变量没被其他线程进行修改,因此可以安全的将新值 N 赋值给变量;若当前变量实际值 V 与期望的旧值 O 不相同,则表明该变量已经被其他线程做了处理,此时将新值 N 赋给变量操作就是不安全的,在进行重试。
4.2 节点类
ConcurrentHashMap
中的节点类包括 Node
、TreeNode
、TreeBin
和 ForwardingNode
。
- Node:实现了
Map.Entry
接口,主要存放键值对,并具有next
域。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
......
}
- TreeNode:树节点,继承自
Node
类,用于红黑树的实现。
**
* Nodes for use in TreeBins
*/
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;
......
}
- TreeBin:封装了
TreeNode
,实际的ConcurrentHashMap
数组中存放的是TreeBin
对象,而不是TreeNode
对象。
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
......
}
- ForwardingNode:在扩容时出现的特殊节点,用于标记正在被迁移的节点。
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
.....
}
4.3 CAS 操作
在 ConcurrentHashMap
中,CAS 操作主要用于以下几个方面:
- 获取数组元素:
tabAt
方法用于获取table
数组中指定索引位置的元素。 - 设置数组元素:
casTabAt
方法用于通过 CAS 操作设置table
数组中指定索引位置的元素。 - 直接设置数组元素:
setTabAt
方法用于直接设置table
数组中指定索引位置的元素。
下面我们详细介绍这些方法的实现。
4.3.1 tabAt
方法
tabAt
方法用于获取 table
数组中索引为 i
的 Node
元素。
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
U.getObjectVolatile
:这是一个Unsafe
类的方法,用于获取数组中指定索引位置的元素,并保证该操作的可见性。((long)i << ASHIFT) + ABASE
:计算数组中元素的内存地址。ASHIFT
和ABASE
是常量,用于计算数组元素的偏移量。
4.3.2 casTabAt
方法
casTabAt
方法利用 CAS 操作设置 table
数组中索引为 i
的元素。
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
U.compareAndSwapObject
:这是一个Unsafe
类的方法,用于执行 CAS 操作。它会比较数组中指定索引位置的元素是否等于c
,如果相等则将其替换为v
,并返回true
;否则返回false
。((long)i << ASHIFT) + ABASE
:计算数组中元素的内存地址。
4.3.3 setTabAt
方法
setTabAt
方法用于直接设置 table
数组中索引为 i
的元素。
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
U.putObjectVolatile
:这是一个Unsafe
类的方法,用于直接设置数组中指定索引位置的元素,并保证该操作的可见性。((long)i << ASHIFT) + ABASE
:计算数组中元素的内存地址。
4.4 方法
ConcurrentHashMap
是 Java 并发包 (java.util.concurrent
) 中的一种线程安全的哈希表实现。它提供了多种方法来支持高效的并发操作。本文将详细介绍 ConcurrentHashMap
的构造方法、初始化方法、插入方法、获取方法、扩容方法以及与大小相关的方法。
4.4.1 构造方法
ConcurrentHashMap
提供了以下五种构造方法:
// 1. 构造一个空的 map,即 table 数组还未初始化,初始化放在第一次插入数据时,默认大小为 16
ConcurrentHashMap()
// 2. 给定 map 的大小
ConcurrentHashMap(int initialCapacity)
// 3. 给定一个 map
ConcurrentHashMap(Map<? extends K, ? extends V> m)
// 4. 给定 map 的大小以及加载因子
ConcurrentHashMap(int initialCapacity, float loadFactor)
// 5. 给定 map 大小,加载因子以及并发度(预计同时操作数据的线程)
ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
我们来看第 2 种构造方法的源码:
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
这段代码的逻辑如下:
- 如果指定的初始容量小于 0,抛出异常。
- 判断指定的初始容量是否超过了允许的最大值,如果超过则取最大值,否则对指定值进行进一步处理。
- 将处理后的容量赋值给
sizeCtl
。
tableSizeFor
方法用于将指定的容量转换为 2 的幂次方数:
private static final int tableSizeFor(int c) {
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
4.4.2 初始化方法 initTable
initTable
方法用于初始化 table
数组:
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(); // 保证只有一个线程正在进行初始化操作
else if (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); // 计算数组中可用的大小:实际大小 n * 0.75
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
代码逻辑如下:
- 如果当前
table
数组还未初始化,多个线程可能会同时进入这个方法。 - 通过 CAS 操作将
sizeCtl
改为 -1,表示正在初始化。 - 初始化
table
数组,并计算数组中可用的大小。 - 将
sizeCtl
更新为新数组的可用大小。
4.4.3 插入方法 put
put
方法调用 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;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
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, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
代码逻辑如下:
- 计算键的哈希值。
- 如果
table
数组还未初始化,调用initTable
方法进行初始化。 - 如果目标位置为空,使用 CAS 操作插入新节点。
- 如果当前正在扩容,调用
helpTransfer
方法协助扩容。 - 如果目标位置不为空,根据节点类型(链表或红黑树)插入新节点。
- 插入完成后,检查链表长度是否需要转换为红黑树。
- 检查当前容量是否需要扩容。
4.4.4 获取方法 get
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) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
代码逻辑如下:
- 计算键的哈希值。
- 检查
table
数组是否已初始化,并定位到目标桶。 - 如果桶的第一个节点与目标键匹配,直接返回该节点的值。
- 如果桶的第一个节点是红黑树节点,调用
find
方法在红黑树中查找。 - 否则,遍历链表查找目标键。
4.4.5 扩容方法 transfer
transfer
方法用于扩容 table
数组:
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
if (nextTab == null) {
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false;
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n;
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true;
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
代码逻辑如下:
- 创建一个新的
nextTable
,容量为原table
的两倍。 - 遍历原
table
中的每个桶,将桶中的元素复制到nextTable
中。 - 如果桶为空,使用 CAS 操作设置
forwardingNode
节点。 - 如果桶中的节点是链表节点,将链表分裂成两个链表,分别放入
nextTable
的对应位置。 - 如果桶中的节点是红黑树节点,将红黑树分裂成两个红黑树,分别放入
nextTable
的对应位置。 - 完成复制后,将
nextTable
设为新的table
,并更新sizeCtl
。
4.4.6 大小相关的方法
ConcurrentHashMap
提供了 size
和 mappingCount
方法来获取元素的数量:
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
public long mappingCount() {
long n = sumCount();
return (n < 0L) ? 0L : n;
}
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;
}
代码逻辑如下:
size
方法返回 Map 中的元素数量,结果被限制在Integer.MAX_VALUE
内。mappingCount
方法返回 Map 中的元素数量,允许返回一个long
值。sumCount
方法计算 Map 的实际大小,使用baseCount
和counterCells
数组来跟踪大小。
5 ConcurrentHashMap
的并发控制
ConcurrentHashMap
通过 CAS 操作和 synchronized
关键字来实现并发控制。CAS 操作是一种乐观锁策略,假设每一次操作都不会产生冲突,当且仅当冲突发生时再去尝试。synchronized
关键字在 JDK 1.8 中经过优化,性能与 ReentrantLock
相当,甚至在某些情况下更优。
6 ConcurrentHashMap
的应用示例
假设我们需要构建一个线程安全的高并发统计用户访问次数的功能,ConcurrentHashMap
是一个很好的选择。以下是一个简单的示例:
import java.util.concurrent.ConcurrentHashMap;
public class UserVisitCounter {
private final ConcurrentHashMap<String, Integer> visitCountMap;
public UserVisitCounter() {
this.visitCountMap = new ConcurrentHashMap<>();
}
// 用户访问时调用的方法
public void userVisited(String userId) {
visitCountMap.compute(userId, (key, value) -> value == null ? 1 : value + 1);
}
// 获取用户的访问次数
public int getVisitCount(String userId) {
return visitCountMap.getOrDefault(userId, 0);
}
public static void main(String[] args) {
UserVisitCounter counter = new UserVisitCounter();
// 模拟用户访问
counter.userVisited("user1");
counter.userVisited("user1");
counter.userVisited("user2");
System.out.println("User1 visit count: " + counter.getVisitCount("user1")); // 输出: User1 visit count: 2
System.out.println("User2 visit count: " + counter.getVisitCount("user2")); // 输出: User2 visit count: 1
}
}
在上述示例中:
- 我们使用了
ConcurrentHashMap
来存储用户的访问次数。 - 当用户访问时,我们通过
userVisited
方法更新访问次数。 - 使用
ConcurrentHashMap
的compute
方法可以确保原子地更新用户的访问次数。 - 可以通过
getVisitCount
方法检索任何用户的访问次数。
7 小结
ConcurrentHashMap
是 Java 并发包中一个高效且线程安全的哈希表实现。它支持完全并发的读取,并且能够在多线程环境下高效地进行写入操作。从 JDK 1.7 到 JDK 1.8,ConcurrentHashMap
的内部实现经历了重大改进,从分段锁机制到 CAS 操作和红黑树的应用,进一步提升了并发性能。
通过本文的介绍,希望读者能够更好地理解 ConcurrentHashMap
的工作原理和应用场景,从而在实际开发中更加高效地使用这一强大的数据结构。
8 思维导图
9 参考链接
吊打Java面试官之ConcurrentHashMap(线程安全的哈希表)
标签:Node,ConcurrentHashMap,数组,int,详解,tab,null From: https://blog.csdn.net/gaosw0521/article/details/144024470