这一章讲得有点泛(不过讲详细了估计就得写四章不止了2333),用的JDK版本也与我的不一样(我的是11)…emmm按着书的思路,本地的JDK源码梳理吧。
ConcurrentHashMap
ConcurrentHashMap简单的说就是并发下使用的HashMap。
为啥不直接用HashMap呢?
一方面是值会丢失(这个倒是好理解,阔能出现覆盖的情况,如果俩线程盯着的是同一个地方的话,就会出现这个问题,而且如果是红黑树可能出现,虽然存了看着没丢失,但是如果再转换回链表估计还会丢几个)。
另一方面就是会引发死循环(1.7扩容时会引发循环链表,1.8据说是红黑树成环(不过我的环境至今没有复现过,或许11修复了?))。
还有一个方面就是写入后另一个线程读值,就算存进去了,大概率也是null,对其他线程不可见嘛。
除非对HashMap加锁,要不然还是老老实实的用ConcurrentHashMap吧。
那为啥不直接对HashMap加锁呢?
效率问题,HashMap直接加锁不就跟HashTable一样锁整表了嘛,而ConcurrentHashMap就不一样,在1.8之前是将原来的Node[] 分成一个一个segment,segment就是段锁。不过1.8之后是用锁每个桶中的头节点,而读是不需要加锁的(红黑树的桶是需要有个读写锁的)。其余结构与HashMap类似。
ConcurrentHashMap初始化
参数含义
天呐和HashMap比多了好多参数(代码也长了3,4倍吧大概)。
还是一点一点的啃吧。
常量:
- MAXIMUM_CAPACITY: 最大表容量,1<<30(int类型的最大值是1<<31-1,又由于需要保持2的幂次方)
- **DEFAULT_CAPACITY:**默认初始表容量,16
- MAX_ARRAY_SIZE: 最大的转换位数组的大小,List所允许的最大大小,int的最大值-8(至于为啥-8.官方的解释是为了存储对象头和在一些机器上减小出错的概率(比如内存溢出))
- DEFAULT_CONCURRENCY_LEVEL: 默认并发级别,不过JDK11未使用,仅为了兼容以前的版本。16.
- LOAD_FACTOR: 老朋友了,负载系数,如果已经使用的桶的数量大于了总容量的0.75,那么久需要扩容了,为啥采用0.75,是空间与时间的一种权衡。
- TREEIFY_THRESHOLD: 链表树化的阈值。为8,取这个值是因为哈希分布基本泊松分布,所以当一个桶中元素数量为8时,已经是一种比较极端的情况了,所以这时采用将链表树化,空间换时间。
- UNTREEIFY_THRESHOLD: 树退化为链表的阈值,为6,正好是高度为2的红黑树最大能达到的值。
- MIN_TREEIFY_CAPACITY: 最小的树化桶表容量,64.也就是说树化不仅得桶中元素大于等于8,还得表容量大于64.
- MIN_TRANSFER_STRIDE: 扩容,转移时每个核心处理的最小的桶表数量。
- RESIZE_STAMP_BITS: 生成每次扩容都唯一的生成戳的数,16
- MAX_RESIZERS: 最大参与扩容的线程数,(1 << (32 - RESIZE_STAMP_BITS)) - 1 = 2^15-1;
- RESIZE_STAMP_SHIFT: 用作扩容后生成戳位移的偏移量。
- MOVED=-1 ForwardingNode的hash值,ForwardingNode用于在扩容时,将读操作转发到新数组上,将写操作的线程去帮忙扩容。
- TREEBIN = -2 :TreeBin的hash值,TreeBin持有某个桶内红黑树的根节点,它存在的目的是,由于红黑树写操作会改变树结构,所以读写时需要维护一个读写锁。
- RESERVED = -3:保留的Hash仅用于computeIfAbsent和 compute
- HASH_BITS = 0x7fffffff 做Hash运算时与其相与,可限定Hash的范围为正。
- NCPU: CPU核心数,也是用于扩容时算每个线程需要转移多少桶。
- ObjectStreamField[] : 为了兼容JDK7以前版本,序列化伪字段。
这个类可以让数组像C语言的指针一样操作内存。
所以名字很直观,如非必须最好别用
private static final Unsafe U = Unsafe.getUnsafe();
sizeCtl的偏移量
private static final long SIZECTL;
transferIndex的偏移量
private static final long TRANSFERINDEX;
baseCount的偏移量
private static final long BASECOUNT;
callsBusy的偏移量
private static final long CELLSBUSY;
cellValue的偏移量
private static final long CELLVALUE;
Node[]的首地址
private static final int ABASE;
Node[]的偏移量
private static final int ASHIFT;
类变量:
transient volatile Node<K,V>[] table;
扩容时的新数组,仅在扩容时存在,换个说法若它不为null,则说明在扩容
private transient volatile Node<K,V>[] nextTable;
计数器基本值,主要在没有碰到多线程竞争时使用,需要通过CAS进行更新
private transient volatile long baseCount;
0:默认值,此时在真正的初始化操作中使用默认容量
> 0,初始化/扩容完成后的容量
<0,正在进行扩容
private transient volatile int sizeCtl;
下一个需要迁移的桶的索引+1
private transient volatile int transferIndex;
CAS自旋锁标志位,用于初始化,或者counterCells扩容时
private transient volatile int cellsBusy;
用于高并发的计数单元,如果初始化了这些计数单元,
那么跟table数组一样,长度必须是2^n的形式
private transient volatile CounterCell[] counterCells;
初始化
一共有五个构造方法。
真正干活的那个方法,的执行逻辑就是
首先特判是否符合规则,
然后看并发级别是否比初始容量大,如果大了,则将初始容量置为并发级别。
再算出大小,(初始容量+1)/0.75.(+1操作减少扩容次数)
接着将大小置为最接近的2的幂次方
最后将sizeCtl设置为大小
public ConcurrentHashMap() {
}
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, LOAD_FACTOR, 1);
}
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) {
特判,负载参数得大于0,初始化容量得大于0,并发级别得大于0
否则抛异常
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的幂次方的写法和HashMap一毛一样
就是求将-1(-1的二进制全是1,右移(31-(c-1)的最高位数)),最后+1,
则正好就是2的幂次方,至于为啥c-1,是为了防止多扩大了2倍,
正好等于2的幂次方时保留原数即可。
private static final int tableSizeFor(int c) {
int n = -1 >>> Integer.numberOfLeadingZeros(c - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
Hash
与HashMap的类似,不过保证了Hash值为正
高低16位异或,使得更多的位数参与进来,使得在桶表中更好的散列开来。
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
定位table中的node位置
与数组长度求余,取得坐标,然后用Unsafe类中的getObjectAcquire方法获取对应地址的Node(估计是为了可见性的同时,提高效率吧)
将hash值与桶表长度取余
tabAt(tab, (n - 1) & h))
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
使用volatile语义获取对应地址的Node
(有点像C语言里的用指针读数组的味道)
return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);
}
其他初始化
在put等操作中如果没有初始化,则回采用这个方法。
只要没有初始化初始化线程就出不去这个死循环
如果cs<0则说明有线程已经初始化并且都在扩容了,则让出时间片
如果SIZECTL为预设值,则说明还未初始化,开始尝试将其赋值,如果成功,则进入初始化流程.
失败了也没关系,说明其他线程已经初始化了,坐享其成就行了。
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
获取sizeCtl,并尝试将它的值设为-1,说明正在进行初始化
竞争不到也没关系,说明有其他线程执行了初始化
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
如果tab还未初始化
if ((tab = table) == null || tab.length == 0) {
如果sc已经被赋值过,
说明前面已经设定了数组的初始长度,
则用这个值初始化数组
如果为默认值(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 {
将sc赋给sizeCtl,
比如:当使用空参构造器时,这步就有必要了
sizeCtl = sc;
}
break;
}
}
return tab;
}
操作
get
所有的操作的基础都是查找,当然先从查找开始啦
首先获取目标key的hash值
然后再判断是否有查找的价值(桶表已经初始化,长度比0大,想要查找的节点所属的桶里有节点)
如果运气贼好的头结点就是目标节点,则返回它的value.
如果它的节点类型为ForwardingNode或者为TreeNode,则用对应的方式去查找,若能找到则返回value,没有则是null。
最后一种情况就是普通链表了,直接遍历,找到则返回value.
都链表/头结点找不到或者没有找的价值返回null.
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
获取hash值
int h = spread(key.hashCode());
如果桶表已经初始化,长度比0大,想要查找的节点所属的桶里有节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
如果头结点key的哈希值等于想要查找key的哈希值
if ((eh = e.hash) == h) {
如果是同一个key,或者key值相同
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
直接返回头结点
return e.val;
}
如果头节点的hash小于0说明在ForwardingNode
或者为红黑树节点
else if (eh < 0)
这里用了多态的机制,是什么类型的节点输出什么find
(父类的find对其隐藏(//说到这个就气))
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;
}
ForwardingNode的find:
当数组正在扩容时便会将已经被迁移的节点置为ForwardingNode。
2:
首先判断是否有查找的必要,没有返回null
1:
如果正好为当前节点,则返回当前节点
如果又被扩容了,则将查找的数组再重置为新的临时数组,再重复之前操作(回到2)。
如果为树节点,则按树的方式查找
最后就是链表的情况,将当前节点置为下一个,重复1:以后所有操作,直到没有节点可查了。
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer:
在新的临时数组中查找
for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
没有查找的价值,直接为null
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
如果正好为头节点,返回头结点
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
如果运气贼背的又被转发了(查找时发生二次扩容了)
if (e instanceof ForwardingNode) {
将tab再置于新的临时数组,回到梦想开始的地方
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
树节点,则按照树节点查找
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
TreeBin的find:
这个TreeBin就像TreeNode的代理一样,所有的操作由它代理了,而TreeNode指提供一个结构以及具体的查找功能。
这么整的目的主要还是为了读写锁的实现方便。
说回find方法:
首先目标key不能为null
然后由于有读写锁的存在(用unsafe里的CAS操作实现的),
如果为写状态,或者等待状态则只能以链表的形式读取节点。
如果发现写完了,则尝试获取锁,并设置为读锁状态,以红黑树的方式查找,查找完后再把状态设置为读锁等待状态。
注意这是有个循环的,以链表的形式只是在当前线程等待或者无法获取时,查找以链表的方式当前节点,
下一次如果可以设置为读锁了则采用红黑树的方式,设置为读锁成功后,肯定就会返回一个值了,无论是null还是value,将当前读锁置于等待状态,最后再唤醒写线程(如果有写线程的话)。
如果还是无法设置为读锁,则还是以链表的方式读取当前节点(上一步的下一个节点)。
final Node<K,V> find(int h, Object k) {
所要求目标key不为null
if (k != null) {
e为头结点
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;
}
TreeNode中的查询:
TreeNode里也有个find方法,不过就是调用的findTreeNode方法,这个方法和HashMap的相比只是多了一个目标key不为null的判断而已。(也就是说HashMap允许一个键为null,ConcurrentHashMap就不允许)。
put操作
put操作就没有查找那么简单了,因为涉及到扩容,扩容的写法很是巧妙呀
看了下这个方法,不仅key不能为null,value也不能。
首先判断key和value是否为null,任一为null抛异常
然后获取添加key的哈希值
再一个循环包括以下添加内容,用于失败重试
如果数组没有初始化则先初始化
如果定位的桶内没有一个节点,则尝试直接添加,如果成功就跳出循环。失败说明被其他线程抢占先机,则重试。
如果发现当前数组在扩容,则先去帮忙扩容,回来再重试
如果不允许更新,且头结点节点与添加的key有一样的key,跳出循环返回对应key的value。
然后再尝试获取该桶的头节点的锁,如果失败被阻塞,如果成功
-如果头结点未被修改
–如果为正常节点(链表),则遍历链表,直到为null,添加
–如果为TreeBin,则以红黑树的方式添加(与HashMap极其相似,仅仅是在需要改变树结构时(一些旋转平衡操作),就会加一个写锁)
–如果为ReservationNode,抛异常。
-如果是链表节点添加,则数目前的链表的长度如果大于等于8则尝试树化。
然后求得节点的数量。
final V putVal(K key, V value, boolean onlyIfAbsent) {
如果key和value至少其一为null则抛出空指针异常
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; K fk; V fv;
如果没有初始化则初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
如果对应的桶中没有节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
则用CAS的方式,将该节点添加进去,
如果失败了则说明被其他线程抢先一步,
这时回到循环开始的地方,重新尝试添加
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
如果当前节点状态为被转发,也就说现在正在扩容,
那就先去帮忙转移,
具体怎么扩容见下
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
只添加不更新的状态下,
检查下头节点,
如果头节点就是想要添加进来的key,则直接返回value
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;
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;
找到了!与添加key一样的key
记录下它的value
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;
}
}
}
如果为TreeBin
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
TreeBin与HashMap中的添加方式及其类似,
只不过在需要调整树结构时会加一个写锁。
如果添加失败,则保存下旧值
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
如果需要更新则更新
if (!onlyIfAbsent)
p.val = value;
}
}
如果为ReservationNode则抛异常,
这个节点只能用于
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
如添加进去了
if (binCount != 0) {
如果说链表的长度大于等于阈值了,
则树化(当然内部会有判断容量是否大于等于64)
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
就是将值加进去,具体的后面再说
addCount(1L, binCount);
return null;
}
帮助扩容:
简单的说就是:
如果能够当前有帮忙转发的需求(还有转发任务待领取和转发还未完成),重复尝试帮助。如果失败了也没关系,说明有其他线程帮忙转发了此次任务了,再尝试,直到没有转发需求。
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
需要扩容转移某些桶
本身tab不为空,
当前节点为转发节点
转发节点指向的临时数组不为空
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
扩容生成戳,根据长度不同会生成唯一的生成戳,为了防止出现ABA问题
(31-当前表长度的最高位数)|1<<(RESIZE_STAMP_BITS - 1)
int rs = resizeStamp(tab.length);
如果扩容还没有完成
具体表现就是数组没有迁移完全,然后sizeCtl<0
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
如果本轮扩容结束,或者没有可以领取的扩容任务了,则跳出.
第一个条件sc与tab.length无法对应上,
也就是说或许遇到ABA了,这时候就不能帮助扩容了
第二个条件:表示本轮扩容结束
第三个条件:表示允许的最大线程参与数已经被占满了
第四个条件:任务已经被领取完了。
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
尝试参与转发任务
如果失败了也没关系说明被其他线程都完成了
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
转发任务:
简单的描述就是:
首先计算本线程需要领取的任务(需要转移的桶)数,如果比预设小,则设为预设。
然后如果临时数组没有初始化,就初始化它(有一个线程初始化成功即可),预处理,根据剩余任务数,判断处理的桶的范围,从后往前分配任务和转移桶,直到完成自己的使命
最后视角转移到具体某个桶的转移工作上来
如果为链表节点,则先保存一个尾链表(就最后全为高节点,或者全为低节点的链表),然后再根据高低节点接上链表,注意使用的头插法(除了那个尾链表)。
如果为红黑树,与HashMap类似,首先分为高低子树(用链表的形式遍历),然后再根据是否能够退化为链表和是否需要重整树来调整。
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; // 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
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;
}
找到用偏移量transferindex,
尝试将它更新为本次线程领取了任务后的剩余任务
(若不足最小领取任务,则全领了)
由此可见,是从高到低分配任务的,转移桶
else if (U.compareAndSetInt
(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;
}
尝试将线程数-1,表明自己退出扩容工作
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
}
}
如果头节点为空,则在该处添加转发节点
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) {
获取高/低位,判断方式与HashMap是一样的
因为扩容后,
取索引的范围大了n也就是位数多了一位
如果想要知道扩容后会不会被迁移到高位
只需要看这一位是否为1即可。
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);
}
CAS的更新临时数组的低高链表,
并将原数组置为转发节点
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;
与HashMap同理串成高低链表,尾插法
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;
}
}
}
}
}
}
size
啊啊啊啊size写完就分析完了。
主要是依赖这个方法,获取counterCell里的值。counterCells里的值都是在每次更新数组时用CAS的方式更新,所以获取的都是最新的值。所以获取时直接相加即可。
具体的分析,下次想起时再写吧,俺懒了。
final long sumCount() {
CounterCell[] cs = counterCells;
long sum = baseCount;
if (cs != null) {
for (CounterCell c : cs)
if (c != null)
sum += c.value;
}
return sum;
}
remove
删除操作和HashMap差别不大,不过多了一些获取头结点锁和一些CAS比对的操作,具体的分析也是等下次想起再写吧。
很多参数的含义借鉴了这篇文章,有兴趣的朋友也阔以看看这个,写得真的很好。
详尽的JDK8的ConcurrentHashMap源码分析