本文章对 java.util.concurrent.ConcurrentHashMap 类在实际生产开发中常用的方法进行解读,若解释有误,还望高手评论区斧正
JDK8的ConcurrentHashMap源码解读
一、介绍
ConcurrentHashMap
是JUC
包下的一个线程安全类,ConcurrentHashMap
并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的安全访问,且尽可能地减少了性能损耗。
public class ConcurrentHashMap<K,V>
extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
二、源码解读
0. 基本属性
// 表初始化和调整大小控制。
// -1:表示table数组正在初始化
// <-1:table数组正在扩容,-2为1个扩容线程,-3为2个扩容线程...
// 0:表示table数组还未初始化
// >0:如果未初始化,表示要初始化的长度,已初始化,表示扩容的阈值
private transient volatile int sizeCtl;
1. 构造方法
public ConcurrentHashMap() {
}
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;
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
2. put相关方法
putVal:存储元素
put方法过程:
- 计算key的哈希值hash
- 如果table未初始化,则CAS进行初始化
- 如果hash在table对应位置的bucket为空,则CAS进行创建(赋值)
- 如果bucket在扩容中,则帮助其扩容
- synchronized锁住bucket的第一个元素,进行put操作
- bucket中的节点数大于等于8,进行table扩容或bucket链表转红黑树
- 调用addCount(),判断是否需要扩容
public V put(K key, V value) {
return putVal(key, value, false);
}
/**
* 如果table或bucket未初始化,则不加锁,通过CAS保证并发安全性
* 其他情况加synchronized锁,锁的是bucket[0]元素
*
* @param key 键
* @param value 新值
* @param onlyIfAbsent true:不存在时才会put
* @return 旧值
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 1.计算key的哈希值hash
int hash = spread(key.hashCode());
int binCount = 0;
for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
// f:bucket存储的根节点数据(链表或红黑树)
// n:table数组长度
// i:key对应的数组索引
// fh:f对应的hash值
ConcurrentHashMap.Node<K,V> f; int n, i, fh;
// 2.如果:table尚未初始化
if (tab == null || (n = tab.length) == 0)
// 初始化table
tab = initTable(); // 初始化表无锁
// 3.如果:bucket尚未使用
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果:CAS创建bucket成功
if (casTabAt(tab, i, null,
new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
// 退出循环,put完成
break;
}
// 4.如果:table[i]处于扩容rehash状态中
else if ((fh = f.hash) == MOVED)
// 帮助table[i]进行扩容,并将tab指向newTable
tab = helpTransfer(tab, f);
else {
// 解决哈希冲突,将元素添加到到链表或红黑树中
V oldVal = null;
// 5.锁住bucket的第一个元素(头节点、根节点),进行put操作
synchronized (f) {
// double check
if (tabAt(tab, i) == f) {
// 如果:bucket为链表结构
if (fh >= 0) {
binCount = 1;
for (ConcurrentHashMap.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;
// 根据onlyIfAbsent选择性更新该value
if (!onlyIfAbsent)
e.val = value;
break;
}
ConcurrentHashMap.Node<K,V> pred = e;
// 如果:遍历到了bucket最后一个node
if ((e = e.next) == null) {
// 创建新node并链接在其尾部
pred.next = new ConcurrentHashMap.Node<K,V>(hash, key, value, null);
break;
}
}
}
// 如果:bucket为树结构
else if (f instanceof ConcurrentHashMap.TreeBin) {
ConcurrentHashMap.Node<K,V> p;
binCount = 2;
if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
// 6.如果:bucket中的节点数大于等于8
if (binCount >= TREEIFY_THRESHOLD)
// 扩容或转为红黑树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 7.
addCount(1L, binCount);
return null;
}
spread:散列算法,获取key的哈希值
// 通过spread方法,获取key的hash值
int hash = spread(key.hashCode());
// spread方法实现
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
// 根据hash值,获取索引位置
int i = (n - 1) & hash
将key的hashcode值,高16位于低16位按 ^ 运算,将结果与数组长度-1进行&运算,得到当前数据需要添加到哪个索引位置
00000110 00011000 00111010 00001110 -h
00000000 00000000 00000110 00011000 -h >>> 16
00000000 00000000 00000000 00000000 -15
01111111 11111111 11111111 11111111 -HASH_BITS
与HASH_BITS进行&运算,最终保证:key的hash值为一个正数
因为负数有特殊含义:
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
initTable:初始化数组
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 判断:数组是否初始化
while ((tab = table) == null || tab.length == 0) {
// 判断:当前table是否正在初始化或扩容
if ((sc = sizeCtl) < 0)
Thread.yield();
// 以CAS方式,将sizeCtl设置为-1(-1表示当前table数组正在初始化)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// Double Check
if ((tab = table) == null || tab.length == 0) {
// sizeCtl > 0,则作为长度,sizeCtl == 0,默认16长度
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// table初始化完成
table = tab = nt;
// 计算下次扩容的阈值,赋值给sc
sc = n - (n >>> 2);
}
} finally {
// sc赋值给sizeCtl
sizeCtl = sc;
}
break;
}
}
return tab;
}
treeifyBin:将bucket的链表转为红黑树
/**
* 替换给定索引处 bin 中的所有链接节点,除非表太小,在这种情况下,改为调整大小。
*/
private final void treeifyBin(ConcurrentHashMap.Node<K,V>[] tab, int index) {
ConcurrentHashMap.Node<K,V> b; int n, sc;
if (tab != null) {
// 1.如果:table长度小于64
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// 扩容为2倍长度
tryPresize(n << 1);
// 2.如果:bucket不为空且为链表结构
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
// 锁住bucket第一个节点
synchronized (b) {
if (tabAt(tab, index) == b) {
ConcurrentHashMap.TreeNode<K,V> hd = null, tl = null;
// 3.将此bucket的所有链表节点转为树节点,并按原顺序将这些树节点组织成双向链表
for (ConcurrentHashMap.Node<K,V> e = b; e != null; e = e.next) {
ConcurrentHashMap.TreeNode<K,V> p =
new ConcurrentHashMap.TreeNode<K,V>(e.hash, e.key, e.val, null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
// 4.将TreeNode由链表结构转化为红黑树结构
setTabAt(tab, index, new ConcurrentHashMap.TreeBin<K,V>(hd));
}
}
}
}
}
tryPresize:扩容
private final void tryPresize(int size) {
// 对扩容数组长度作判断,保证其不超过阈值,并且是2的n次幂
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
// 两种可能:1.未初始化数组(putAll方法)2.初始化数组
Node<K,V>[] tab = table; int n;
// 判断:数组是否初始化
if (tab == null || (n = tab.length) == 0) {
// 进行初始化
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
// 如果扩容长度小于扩容阈值
// 数组长度已经大于等于最大长度
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
// 扩容
else if (tab == table) {
// 获取扩容戳(32位数值,高16位为扩容标识,低16位为扩容线程数)
int rs = resizeStamp(n);
// sc小于0,当前正在扩容
if (sc < 0) {
// 帮助扩容
Node<K,V>[] nt;
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 当前没有正在扩容,设置扩容标识,开始扩容
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
3. get相关方法
public V get(Object key) {
Node<K,V> e;
// 计算key的哈希值,并进行查找相应value节点,找不到则返回null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 如果:表已初始化,且hash对应bucket不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果:bucket第一个元素与该key相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
// 直接返回第一个元素
return first;
if ((e = first.next) != null) {
// 如果是树结构
if (first instanceof TreeNode)
// 以红黑树的检索方式get
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 普通链表结构进行遍历查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 不存在该key
return null;
}
三、其他
关于扩容的概述
在JDK 1.8的ConcurrentHashMap中,迁移元素是扩容过程中的关键步骤,它涉及将旧数组(或称为table)中的元素重新哈希并移动到新的、容量更大的数组中。以下是迁移元素的详细过程:
-
创建新数组:首先,ConcurrentHashMap会创建一个新的数组,其长度通常是旧数组的两倍。这个新数组将用于存储扩容后的所有元素。
-
分配迁移任务:由于ConcurrentHashMap支持并发扩容,多个线程可以同时参与迁移过程。因此,系统会根据当前可用的CPU核心数(NCPU)来分配迁移任务。每个线程负责迁移旧数组中的一段元素,这个段的长度用stride(步长)来表示。为了降低资源竞争频率,stride的最小范围长度通常是固定的,如16。
-
记录迁移进度:在迁移过程中,ConcurrentHashMap使用一个变量(如transferIndex)来记录整个数组扩容的进度。每个参与迁移的线程会根据其负责的段和stride来更新transferIndex的值,以表示当前已经迁移了哪些元素。
-
重新哈希并迁移元素:每个线程会遍历其负责的旧数组段,并对每个元素进行以下操作:
- 计算元素在新数组中的索引位置:这通常是通过重新哈希元素的键来完成的。
- 使用CAS(Compare-And-Swap)或其他原子操作将元素添加到新数组的正确位置。由于多个线程可能同时尝试迁移同一个元素,因此需要使用原子操作来确保数据的一致性。
- 如果CAS操作失败(即有其他线程已经成功迁移了该元素),则当前线程会重试或放弃该元素的迁移。
-
处理冲突和链表:在迁移过程中,如果两个或多个元素哈希到新数组的同一个位置(即发生哈希冲突),它们将被组织成一个链表。如果链表长度过长(超过某个阈值,如8),ConcurrentHashMap可能会将其转换为红黑树以提高查询效率。
-
更新引用和清理:当所有元素都成功迁移到新数组后,ConcurrentHashMap会更新其内部对数组的引用,使其指向新的数组。同时,它会释放与旧数组相关的资源,如内存等。
需要注意的是,由于ConcurrentHashMap的扩容过程涉及多个线程并发执行,因此在迁移元素的过程中可能会出现一些竞争条件和数据不一致的问题。为了解决这些问题,ConcurrentHashMap在迁移过程中使用了一系列复杂的并发控制策略,如CAS操作、自旋锁等,以确保数据的一致性和并发访问的效率。
更多ConcurrentHashMap源码解读,推荐查看文章:https://cloud.tencent.com/developer/article/2209609