先上总结
本文是按照JDK8的源码分析.
- 使用
compareAndSwap
- 利用
LongAdder
原理 - 当冲突的元素达到一定数量时, 使用
红黑树
管理冲突的元素; 较少时, 仍然使用链表 - 扩容时, 并不是粗暴的让其它线程等待, 而是让它们参与扩容过程中, 达到加速效果
数据结构
初始状态16个槽(数组形式), 每个槽中都是相似hash(不是相同)的元素. 元素数量达到阈值时, 会以翻倍的形式进行扩容
预备知识
compareAndSwap原理 https://blog.csdn.net/wzj_whut/article/details/86772268
cpu缓存问题 https://blog.csdn.net/wzj_whut/article/details/86774650
LongAdder原理 https://blog.csdn.net/wzj_whut/article/details/86775838
红黑树, 网上到处都是资料.
源码解读
源码我也只看懂了部分.
成员变量
//主要存储区域, 冲突的元素, 会链表t和树的形式挂在Node上.
transient volatile Node<K,V>[] table;
//扩张的时候所使用的临时表
private transient volatile Node<K,V>[] nextTable;
//ConcurrentHashMap又重造了轮子LongAdder, 自己去看上文中的[LongAdder原理]
private transient volatile int cellsBusy;
private transient volatile long baseCount;//
private transient volatile CounterCell[] counterCells;
hash处理
也这也是重点, 否则无法理解重新分表
的处理过程
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
ConcurrentHashMap对key的hash重新处理了. 为什么这么做?
因为进行分表时, 它只利用了hash值的低位, 因此将hash的高位与低位异或, 可以减少hash的冲突的可能性.
初始状态下, 表的数量为16 (即:
2
4
2^4
24), 会取hash的低4位进行分表.
假设有以下两个hash值 (spread处理之后的值)
h1 = xxxx00110
2
h2 = xxxx10110
2
它们的低4位, 都是0110
, 因此, 当表的数量为16时, 这两个值会分配到同一个槽中(0110
为槽6).
扩容
当map的元素数量达到阈值时, 会扩容. 首次扩容后槽的数量为32 , hash值的低5位用于分配.
设扩容前槽的数量为n.
h1仍分配到槽6
h2将分配到22. (可以写为 h1 & (n-1) + 16)
initTable
初始化分区表
这个接口内部及调用它的接口, 都未对初始化过程
加锁, 那么它是如何保证多线程安全的呢?
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0){
//使用临时变量存储sizeCtl, 因为sizeCtl会被作为一个标志位, 用于线程同步
//如果 sizeCtl<0, 则说明另外一个线程正在执行初始化, 本线程将等待其初始化完成
Thread.yield(); // lost initialization race; just spin
}else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//将sizeCtl置为-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*0.75 = 12
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
tabAt
使用unsafe
接口读取数组元素, 我猜测其中的一个原因是: 这样做比使用java中的下标操作array[i]
更快.
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);
}
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)
//无锁方式初始化table, 上面已经讲过
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//初始化table数组中的元素. 通过compareAndSwap方式
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) {
//正式执行put操作, 这此操作synchronized, 以确保100%线程安全.
if (tabAt(tab, i) == f) {
//f并不是通过线程安全的方式取得的, 因此必须再判断一次
if (fh >= 0) { //此时,Node是普通的链表形式
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
//遍历表中的内容
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
//发现key已经存在了
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是红黑树形式
Node<K,V> p;
binCount = 2; //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){
//链表形式的table, 元素数据达到阈值之后, 转化为红黑树形式
treeifyBin(tab, i);
}
if (oldVal != null){
//本次操作, 使用新值替换了旧值, map中元素的数量不变
return oldVal;
}
break;
}
}
}
//新增了一个元素, 总数+1
addCount(1L, binCount);
return null;
}
helpTransfer
当前线加入到扩容过程中.
addCount
修改map的size计数, 例如put操作需要+1, remove操作需要-1. 如果是增长, 那么就有可能需要扩容, 增加更多的分区.
先看看LongAdder原理 https://blog.csdn.net/wzj_whut/article/details/86775838
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//通过[LongAdder原理]修改总数, 看我的博客https://blog.csdn.net/wzj_whut/article/details/86775838
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1){
//不需要扩容. 场景有:
// 1. 删除元素或替换元素
// 2. 元素数量较少,还在使用链表结构
return;
}
//可能需要扩容, 取出总数
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//sizeCtl为总容量的0.75
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) { //MAXIMUM_CAPACITY= 2的30次方
// rs = Integer.numberOfLeadingZeros(n) | (1 << 15);
// 因为n为2的幂, 若n变化, 那么肯定是n向左移位了, numberOfLeadingZeros(n)的值就必然变化
// 最后将numberOfLeadingZeros(n)存放在rs的低15位上.
// 可以这么认为: rs低15存放了n的特征值,其它的位用来做flag.
//
int rs = resizeStamp(n);
if (sc < 0) {//负数表示正在初始化,或是正在扩容中
//sc<0时, sc == (rs << 16) + 2)
//sc>>>16之后,得到rs
//若rs不同,说明其它线程正在执行transfer(tab, null)
//若rs相同,但sc == rs + 1,说明其它线程正在执行 transfer(tab, nt)
//若rs相同,sc != rs +1 , 但是sc == rs + ((1<<16)-1). 这个我也不懂
//nextTable==null,
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)){
//本线程加入到扩容过程中. 此时sc是一个负数.
transfer(tab, nt);
}
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2)){
//说明当前线程是本轮扩容任务中的头号玩家.
transfer(tab, null);
}
s = sumCount();
}
}
}
transfer
扩容过程, 这是最精华的部分. 我也一知半解
参考: https://www.cnblogs.com/stateis0/p/9062086.html
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//stride为每次处理的槽的数量.
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE){
stride = MIN_TRANSFER_STRIDE; // subdivide range
}
if (nextTab == null) { // initiating
try {
//槽的数量翻倍.没什么好说的.
@SuppressWarnings("unchecked")
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;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
//transferIndex用于维护处理的进度.
//当线程运行到此处时, 根据transferIndex领取需要执行迁移操作的槽, 然后执行迁移操作
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; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) { //表示是链表形式
//runBit要么是n, 要么是0
//为0时, 新的hash未变,
//为n时, 表明hash会出现变化
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) {
//红黑树中的节点也应该是按照hash值的规律排列的, 我还没看.
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;
}
}
}
}
}
}