参考:Java多线程进阶(二三)—— J.U.C之collections框架:ConcurrentHashMap结构
JDK
版本:AdoptOpenJDK 11.0.10+9
本文是在学习了 这篇博文 之后整理的相关知识点,这篇博文已经讲解的相当好了。
1 基本概念
ConcurrentHashMap
(发音:肯卡润特哈希迈普
)是1.5
的时候引入的,是一个线程安全的HashMap
。
ConcurrentHashMap
的结果非常复杂,其中有一些基本结构需要好好理解。
1.1 桶
ConcurrentHashMap
内部维护了一个Node
类型的数组table
:
transient volatile Node<K,V>[] table;
数组的每一个位置table[i]
代表了一个桶。
当插入键值对的时候,会根据key
的hash
值计算出对应的桶的位置,将value
加入到对应的桶里面。
table
一共可以包含4
种不同类型的桶,都是Node
或其子类:
Node
类型TreeBin
类型,连接的是一颗红黑树,树节点类型为TreeNode
ForwardingNode
类型ReservationNode
类型
1.2 Node节点
Node
节点是ConcurrentHashMap
中最基本的节点,是其他类型节点的父节点。
默认链接上
table[i]
桶上的节点就是Node
节点。当出现
hash
冲突的时候,Node
节点会首先以 链表 的方式链接到table[i]
上;当节点超过一定数量的时候,链表会转化为红黑树。
下面是Node
的源码:
/**
* 一个Node节点保存一个“键值对”,在链表结构中使用
*/
static class Node<K,V> implements Map.Entry<K,V> {
// 哈希值
final int hash;
// 键
final K key;
// 值
volatile V val;
// 链表的指针,指向下一个Node节点
volatile Node<K,V> next;
Node(int hash, K key, V val) {
this.hash = hash;
this.key = key;
this.val = val;
}
Node(int hash, K key, V val, Node<K,V> next) {
this(hash, key, val);
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString() {
return Helpers.mapEntryToString(key, val);
}
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u; Map.Entry<?,?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
1.3 一些常量
ConcurrentHashMap
内部定义了一些常量。
/**
* 最大的容量,也就是2^30。
* (在ConcurrentHashMap中,容量定义为2的幂,
* 而Integer最大值为2^31 - 1,所以容量的最大值只能取到2^30)
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认初始容量,为16
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* table数组的最大长度
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 默认的并发级别,为16
*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* 负载因子,默认为0.75,为了兼容1.8以前的版本而留下的
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* 链表转化为树的阈值。链表上Node节点大于等于8个的时候,链表将会转化为树。
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 树转化为链表的阈值。树上节点小于等于6个的时候,树转化为链表。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 在链表转化为树之前,还会有一次判断:
* 只有table数组的长度大于64,才会发生转化。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 在树转变为链表之前还会有一次判断:
* 只有键值对的数量小于16,才会发生转换。
*/
private static final int MIN_TRANSFER_STRIDE = 16;
/**
* 用于在扩容时生成唯一的随机数
*/
private static final int RESIZE_STAMP_BITS = 16;
/**
* 可以同时进行扩容操作的最大线程数
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/**
* The bit shift for recording size stamp in sizeCtl.
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
/*
* Encodings for Node hash fields. See above for explanation.
*/
static final int MOVED = -1; // 标识ForwardingNode节点
static final int TREEBIN = -2; // 标识红黑树节点
static final int RESERVED = -3; // 标识ReservationNode节点
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
/** CPU核心数,扩容时使用 */
static final int NCPU = Runtime.getRuntime().availableProcessors();
1.4 一些字段
ConcurrentHashMap
中定义了一些字段。
/**
* Node数组,代表整个Map,在首次插入键值对的时候创建,大小总是2的幂。
*/
transient volatile Node<K,V>[] table;
/**
* 扩容后的新table数组,只有在扩容的时候才使用
*/
private transient volatile Node<K,V>[] nextTable;
/**
* 计数基数,当没有线程竞争的时候,计数将加到这个变量上
*/
private transient volatile long baseCount;
/**
* 控制table的初始化和扩容:
* 0 : 初始化的默认值;
* -1 :表示有线程正在进行table的初始化;
* > 0 :表示table初始化时使用的容量,或者初始化/扩容完成之后的threshold;
* -(1 + nThreads):表示正在执行扩容任务的线程数;
*/
private transient volatile int sizeCtl;
/**
* 扩容时需要用到的一个下标变量
*/
private transient volatile int transferIndex;
/**
* 自旋标识位,用于CounterCell[]扩容时使用
*/
private transient volatile int cellsBusy;
/**
* 计数数组,出现并发冲突的时候使用
*/
private transient volatile CounterCell[] counterCells;
// 试图字段
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
2 构造函数
ConcurrentHashMap
提供了5
个构造函数。
ConcurrentHashMap
采用 “懒加载” 模式,只有到首次插入键值对的时候,才会真正的初始化table
数组。
2.1 空构造函数
public ConcurrentHashMap() {
}
只创建ConcurrentHashMap
对象,不做任何初始化。
2.2 指定table初始容量、负载因子、并发级别的构造函数
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;
}
注意:concurrencyLevel
只是为了兼容1.8
以前的版本,并不是实际的并发级别,loadFactor
也不是实际的负载因子,这两个都失去了原来的意义,仅仅对初始化容量有一定的控制作用。
2.3 指定table初始容量的构造函数
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, LOAD_FACTOR, 1);
}
负载因子默认使用的是常量LOAD_FACTOR
,值为0.75
。并发级别默认为1
。
2.4 指定table初始容量、负载因子的构造函数
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
这里,默认并发级别是1
。
2.5 根据已有的map构造
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
这里使用的初始化容量为DEFAULT_CAPACITY
,值为16
。然后保存所有的键值对。
3 重要的方法
下面对ConcurrentHashMap
中最终要的也是最常用的几个方法进行分析:
put
操作get
操作size
计算集合大小
3.1 put插入键值对
put
插入一个键值对,key
和value
均不能为null
。
public V put(K key, V value) {
return putVal(key, value, false);
}
实际调用的时putVal(key, value, false)
函数:
/**
* onlyIfAbsent == true,仅当key不存在的时候才会插入键值对
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key和value不能为null
if (key == null || value == null) throw new NullPointerException();
// 计算出key对应的hash值
int hash = spread(key.hashCode());
/**
* 使用链表保存时,binCount记录table[i]这个桶中所保存的结点数;
* 使用红黑树保存时,binCount == 2,保证put后更改计数值时能够进行扩容检查,同时不触发红黑树化操作
*/
int binCount = 0;
// 自旋插入结点,直到成功
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
// CASE1: 首次插入键值对时,需要初始化table —— 懒加载
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// CASE2: table[i]对应的桶为null
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 插入一个链表结点
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
// CASE3: 发现是ForwardingNode结点,说明此时table正在扩容,则尝试协助数据迁移
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
// CASE4: 出现hash冲突,也就是table[i]桶中已经曾经添加了Node节点
else {
V oldVal = null;
// 锁住table[i]结点
synchronized (f) {
// 再判断一下table[i]是不是第一个结点, 防止其它线程的写修改
if (tabAt(tab, i) == f) {
// CASE4.1: table[i]是链表结点
if (fh >= 0) {
binCount = 1;
// 找到“相等”的结点,判断是否需要更新value值
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);
break;
}
}
}
// CASE4.2: table[i]是红黑树结点
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;
}
}
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;
}
}
}
// 计数值加1
addCount(1L, binCount);
return null;
}
put
添加键值对的大致流程描述如下:
- 首先计算出
key
的hash
值,然后根据i = (n - 1) & hash
找到table
数组中桶的位置i
; - 如果是第一次插入,需要对
table
初始化; - 如果
table[i]
是null
,那么直接通过CAS
插入一个新的Node
到table[i]
桶下面; - 如果发现
table[i]
是ForwardingNode
节点,说明此时table
正在扩容,则尝试协助数据迁移; - 如果发生了
hash
冲突,说明table[i]
桶中已经曾经添加过Node
节点,需要用synchronized
锁住table[i]
节点,并开始解决冲突问题。如果table[i]
下面是链表,那么从第一个节点开始遍历,如果找到了和插入节点“相等”的节点,那么根据onlyIfAbsent
的值决定是否需要更新节点的value
;如果遍历到了尾节点,还没有找到“相等”的节点,那么采用“尾插法”在链表的尾部添加新的Node
节点。如果table[i]
下面是红黑树,调用putTreeVal
方法将节点添加到树中。 - 完事了,要看一下table[i]下面链表中的节点个数是否超过了阈值(
8
个),如果超过了,就要将链表转化为红黑树。
3.1.1 初始化table数组
注意,在首次插入键值对时,需要初始化table
—— 懒加载。
/**
* 初始化table,使用sizeCtl作为初始化容量。
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 自旋,直到初始化成功
while ((tab = table) == null || tab.length == 0) {
// 如果sizeCtl小于0, 说明table已经正在初始化或者扩容中
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 通过CAS将sizeCtl值更新为-1,表示table正在初始化
else if (U.compareAndSetInt(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;
// 这里实际上是0.75*n
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
初始化table
数组的时候,会将sizeCtl
作为table
数组的大小,这个值是0.75*n
,也就是3/4
的数组大小,这样相当于设置了一个阈值。
3.1.2 链表转红黑树
当链表中的节点个数达到阈值(8
个),会将链表转化为红黑树:
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n;
if (tab != null) {
// CASE 1: table的大小 < MIN_TREEIFY_CAPACITY(64)时,直接进行table扩容,不进行红黑树转换
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
// CASE 2: table的大小 ≥ MIN_TREEIFY_CAPACITY(64)时,进行链表 -> 红黑树的转换
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;
}
// 以TreeBin类型包装,并链接到table[index]中
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
3.2 get获取value
get
通过key
来获取对应的value
,找不到则返回null
:
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算key的hash值
int h = spread(key.hashCode());
// 根据i = (n - 1) & hash找到table[i]桶位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// CASE 1: 如果table[i]这个节点就是要找的数据,则直接返回value
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// CASE 2: 如果table[i]的节点的hash值小于0, 说明遇到了特殊节点,调用find方法查找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// CASE 3: 如果是链表,按链表的方法查找,从第一个节点遍历,找到就返回value
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
get
获取数据的大致流程描述如下:
- 首先计算
key
的hash
值,然后根据i = (n - 1) & hash
找到table
数组中桶的位置i
; - 如果
table[i]
的key
和待查找的key
相同,直接返回table[i]
这个节点的value
; - 如果
table[i]
对应的节点是特殊节点(hash
值小于0
),则通过find
方法查找节点; - 如果
table[i]
对应的节点是普通的链表节点,则按照链表的方式从头遍历。
3.2.1 find查找节点
在CASE 2
的情况下,如果table[i]
是特殊节点,那么需要调用对应节点的find
方法来查找。
Node
节点的find
/**
* Node节点的find,实际上是链表的查找
*/
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
当table[i]
节点是Node
节点的时候,说明是链表结构,那么直接从表头开始遍历查找。
TreeBin
节点的find
TreeBin
的查找比较特殊,我们知道当槽 table[i]
被TreeBin
结点占用时,说明链接的是一棵红黑树。由于红黑树的插入、删除会涉及整个结构的调整,所以通常存在读写并发操作的时候,是需要加锁的。
ConcurrentHashMap
采用了一种 类似读写锁 的方式:当线程持有写锁(修改红黑树)时,如果读线程需要查找,不会像传统的读写锁那样阻塞等待,而是转而以链表的形式进行查找(TreeBin
本身是Node
类型的子类,拥有Node
的所有字段)。
final Node<K,V> find(int h, Object k) {
if (k != null) {
for (Node<K,V> e = first; e != null; ) {
int s; K ek;
if (((s = lockState) & (WAITER|WRITER)) != 0) {
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
}
else if (U.compareAndSetInt(this, LOCKSTATE, s,
s + READER)) {
TreeNode<K,V> r, p;
try {
p = ((r = root) == null ? null :
r.findTreeNode(h, k, null));
} finally {
Thread w;
if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
(READER|WAITER) && (w = waiter) != null)
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}
ForwardingNode
节点的find
ForwardingNode是一种临时结点,在扩容进行中才会出现,所以查找也在 扩容的table
上进行。
ReservationNode
结点的find
ReservationNode
是保留节点,不保存实际数据,直接返回null
。
3.3 size计算大小
调用ConcurrentHashMap
的size()
函数,可以获得键值对的个数:
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
实际上,调用的是sumCount()
函数:
final long sumCount() {
CounterCell[] cs = counterCells;
long sum = baseCount;
if (cs != null) {
for (CounterCell c : cs)
if (c != null)
sum += c.value;
}
return sum;
}
可以看到,sum
的值分两部分:
baseCount
的值- 如果
CounterCell[]
数组中每个对象的value
的总和
公式如下:
s
u
m
=
b
a
s
e
C
o
u
n
t
+
∑
i
=
0
n
C
o
u
n
t
e
r
C
e
l
l
[
i
]
sum = baseCount + \sum_{i=0}^{n}{CounterCell[i]}
sum=baseCount+i=0∑nCounterCell[i]
3.3.1 计数原理
ConcurrentHashMap
中定义了几个和计数相关的字段:
/**
* 计数基数,当没有线程竞争的时候,计数将加到这个变量上
*/
private transient volatile long baseCount;
/**
* 自旋标识位,用于CounterCell[]扩容时使用
*/
private transient volatile int cellsBusy;
/**
* 计数数组,出现多线程并发冲突的时候使用
*/
private transient volatile CounterCell[] counterCells;
其中,在没发生多线程并发冲突的时候,计数将会加到baseCount
上;当发生并发冲突的时候,计数将会加到counterCells
数组上。
CounterCell
的结构如下,其内部只有一个value
属性,并且用volatile
修饰:
@jdk.internal.vm.annotation.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
回归之前在put
键值对的时候,在putVal
函数的最后,调用了addCount(1L, binCount)
将计数值加1
,源码如下:
private final void addCount(long x, int check) {
CounterCell[] cs; long b, s;
// 如果counterCells是null,说明之前一直没有发生并发冲突,那么直接将值累加到baseCount上
if ((cs = counterCells) != null ||
!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// 否则,那就是曾经发生过并发冲突,那么将值累加到对应的CounterCell槽
CounterCell c; long v; int m;
boolean uncontended = true;
// cs[ThreadLocalRandom.getProbe() & m])这里是根据线程的hash计算槽的位置,
// 通过compareAndSetLong更新槽的值
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
// 如果更新槽的值失败,说明槽中也出现了并发冲突,
// 可能涉及槽数组counterCells的扩容,所以调用fullAddCount方法
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
// 检测是否扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSetInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
4 扩容和数据迁移
前面的源码中提到,当table[i]
桶中Node
节点个数大于等于TREEIFY_THRESHOLD
(8
个)的时候,将通过调用treeifyBin
函数将链表转化为树:
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 链表转化为树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
但是这个转化不一定真的执行,查看treeifyBin
源码:
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
......
}
}
可以看到,又进行了一次判断:如果table
数组的长度小于MIN_TREEIFY_CAPACITY
(值为64
),调用tryPresize(n << 1)
进行一次扩容,将table
的长度扩大为原先的2
倍。
4.1 扩容的基本思路
在ConcurrentHashMap
中,扩容分为两步:
4.1.1 table数组的扩容
一般是新建一个2
倍大小的新table
数组,这个过程由一个单线程完成,不允许出现并发。
4.1.2 数据迁移
数据迁移就是把旧table
中的数据重新分配到新的table
中。
因为节点的i = (n - 1) & hash
,新的数组大小n
变了,所以这一过程涉及到每个Node
节点重新计算桶位置i
。
ConcurrentHashMap
在处理数据迁移的时候,并不会重新计算每个key
的桶位置,而是利用一种很巧妙的方法。
因为
ConcurrentHashMap
中规定table
数组的的大小必须得是2
的幂。当
table
扩容后(大小为原来的2
倍),通过i = (n - 1) & hash
计算出来的新的i
值,要么等于原来的i
的值,要么等于i + n
。
也就是说,旧数组中table[i]
下的节点,一部分会被分配到新数组的table[i]
下面,一部分会被分配到新数组的table[i+n]
下面。这样就可以用“分治”的方法,可以多线程同时处理数据迁移。
4.2 扩容源码分析
下面来看看扩容的具体实现,查看tryPresize
函数的源码:
/**
* 尝试预调整table数组的大小为指定的size大小
*/
private final void tryPresize(int size) {
// 视情况将size调整为2的幂
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
// CASE 1: table数组还没初始化,则先进行初始化
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
// 初始化table
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
// CASE 2: C <= SC, 说明已经被扩容过了,n >= MAXIMUM_CAPACITY说明数组已经达到最大容量
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
// CASE 3: 进行table数组扩容
else if (tab == table) {
// 根据容量n生成一个随机数,唯一标识本次扩容操作
int rs = resizeStamp(n);
// 这个CAS操作可以保证,仅有一个线程会执行扩容
if (U.compareAndSetInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 扩容和数据迁移
transfer(tab, null);
}
}
}
大致原理:
- 如果
table
数组没有初始化,则进行初始化; - 如果已经被扩容了,或者数组已经达到最大容量了,直接退出:
- 调用
transfer(tab, null)
函数进行扩容和数据迁移。
接着,查看transfer
源码,这个函数可以多个线程并发调用:
// tab代表旧的table,nextTab代表扩容后的新table
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// stride可理解成“步长”,即数据迁移时,每个线程要负责旧table中的多少个桶
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// nextTab == null,说明是首次进行扩容
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
// 创建新数组,大小为旧数组的2倍
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
// [transferIndex-stride, transferIndex-1]表示当前线程要进行数据迁移的桶区间
transferIndex = n;
}
int nextn = nextTab.length;
// ForwardingNode结点,当旧table的某个桶中的所有结点都迁移完后,用该结点占据这个桶
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 标识一个桶的迁移工作是否完成,advance == true 表示可以进行下一个位置的迁移
boolean advance = true;
// 最后一个数据迁移的线程将该值置为true,并进行本轮扩容的收尾工作
boolean finishing = false; // to ensure sweep before committing nextTab
// i标识桶索引, bound标识边界
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 每一次自旋前的预处理,主要是定位本轮处理的桶区间
// 正常情况下,预处理完成后:i == transferIndex-1,bound == transferIndex-stride
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSetInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// CASE 1: 当前是处理最后一个tranfer任务的线程或出现扩容冲突
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 如果所有桶迁移均已完成
if (finishing) {
nextTable = null;
// table指向新的数组
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 扩容线程数减1, 表示当前线程已完成自己的transfer任务
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 判断当前线程是否是本轮扩容中的最后一个线程,如果不是,则直接退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
// CASE2:旧桶本身为null,不用迁移,直接尝试放一个ForwardingNode
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// CASE3:该旧桶已经迁移完成,直接跳过
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
// CASE4:该旧桶未迁移完成,进行数据迁移
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// CASE4.1:fh是table[i]的hash值,桶的hash > 0,说明是链表迁移
if (fh >= 0) {
/**
* 下面的过程会将旧桶中的链表分成两部分:ln链和hn链
* ln链会插入到新table的槽i中,hn链会插入到新table的槽i+n中
*/
// 由于n是2的幂次,所以runBit要么是0,要么高位是1
int runBit = fh & n;
// lastRun指向最后一个相邻runBit不同的结点
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;
}
// 以lastRun所指向的结点为分界,将链表拆成2个子链表ln、hn
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);
}
// ln链表存入新桶的索引i位置
setTabAt(nextTab, i, ln);
// hn链表存入新桶的索引i+n位置
setTabAt(nextTab, i + n, hn);
// 设置ForwardingNode占位
setTabAt(tab, i, fwd);
// 表示当前旧桶的结点已迁移完毕
advance = true;
}
// CASE4.2:红黑树迁移
else if (f instanceof TreeBin) {
/**
* 下面的过程会先以链表方式遍历,复制所有结点,然后根据高低位组装成两个链表;
* 然后看下是否需要进行红黑树转换,最后放到新table对应的桶中
*/
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);
// 设置ForwardingNode占位
setTabAt(tab, i, fwd);
// 表示当前旧桶的结点已迁移完毕
advance = true;
}
}
}
}
}
}
下面重点强调扩容中的集合要点。
4.2.1 步长的计算
stride
是要计算的步长,即数据迁移时,每个线程要负责旧table
中的多少个桶:
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
4.2.2 首次扩容需要初始化新的table
如果是首次扩容,先初始化新的table
:
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
// 旧table的大小是n,新table大小初始化为2n
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
4.2.3 每个线程处理的范围
注意上面的 transferIndex
,大小等于n
。
table[transferIndex - stride, transferIndex - 1]
就是当前线程要进行数据迁移的桶区间。 整个 transfer
方法几乎都在一个自旋操作中完成,从右往左 开始进行数据迁移,transfer
的退出点是当某个线程处理完最后的table
区段—— table[0, stride - 1]
。
4.2.4 链表数据的迁移
在CASE 4
中,需要分情况处理。
如果
table[i]
的hash
大于0
,说明table[i]
下面连接的是一个链表,进行链表数据的迁移。
链表迁移的过程如下:
-
首先会遍历一遍原链表,找到最后一个相邻
runBit
不同的结点。runbit
是根据key.hash
和旧table
长度n
进行与运算得到的值,由于table
的长度为2
的幂次,所以runbit
只可能为0
或最高位为1
。 -
然后,会进行第二次链表遍历。按照第一次遍历找到的结点为界,将原链表分成
2
个子链表ln
和hn
。 -
最后将
ln
和hn
这两个链表连接到新的table
桶中,ln
迁移到新的table[i]
位置,hn
迁移到新的table[i+n]
位置。
4.2.5 红黑树的迁移
如果
table[i]
的节点类型是TreeBin
,说明table[i]
下面连接的是一个红黑树。
红黑树的迁移过程如下:
-
首先会按照链表遍历的方式(
e = e.next
)去遍历所有节点。在遍历的过程中,会根据每个节点的hash
值进行计算(hash & n
),根据这个计算结果是不是等于0
,将红黑树的节点分成两个链表lo
和hi
; -
然后会根据是否到达阈值,来决定
lo
和hi
应该是以链表的形式还是红黑树的形式加入到新的table
中; -
最后分别插入到新
table
的table[i]
和table[i+n]
下面。