重要的一些变量
//数组最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
//数组默认容量
private static final int DEFAULT_CAPACITY = 16;
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//转换因子,一般是求阈值的时候用数组长度*转换因子
private static final float LOAD_FACTOR = 0.75f;
//链表转换红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
//红黑树转换链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//最小红黑树容量
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
//标识该节点正在迁移
static final int MOVED = -1;
//标识该节点是树节点
static final int TREEBIN = -2;
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
//获取系统的CPU核数
static final int NCPU = Runtime.getRuntime().availableProcessors();
put方法分析
我们先看下流程图:
OK,看了流程图,脑海大概有个映象,大概有以下几点:
- 数组不存在的时候初始化数组。
- 数组下标位置节点不存在,则直接创建一个新的节点放进数组就可以。
- 数组下标位置存在节点,并且该节点正在进行节点的迁移,则当前线程就先帮助节点进行迁移,再进行相应的新增节点操作。
- 数组下标位置存在节点且当前线程获得当前节点的所有权,如果该节点是链表形式则直接插到链表尾,如果是树节点,则跟链表一样。
- 如果一个数组下标位置处的链表节点超过8个,但是数组的长度小于最小数组长度64则对数组容量扩容,一般是扩容为原来的2的n次方倍;如果节点处链表节点超过8个并且数组的长度大于等于最小数组长度64,则进行红黑树化,将新的树设置到数组对应下标处。
大概总结了下,现在我们对put的源码进行分析:
//往map加入数据
public V put(K key, V value) {
//onlyIfAbsent=true,只有在不存在该key时才会进行put操作
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
//key和value都不能为空
if (key == null || value == null) throw new NullPointerException();
//求hash
int hash = spread(key.hashCode());
//存储链表元素个数
int binCount = 0;
for (Node[] tab = table;;) {
Node f; int n, i, fh;
//如果数组还没有被创建
if (tab == null || (n = tab.length) == 0)
//初始化数组
tab = initTable();
//以volatile的形式获取,数组的最后一个位置没节点的话,直接创建node放进去
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node(hash, key, value, null)))
break;
}
//如果是MOVED,说明正在扩容,去帮助迁移
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 到这里就是说,f 是该位置的头结点,而且不为空
else {
V oldVal = null;
//锁定当前节点,防止并发
synchronized (f) {
//读取i位置下的节点是不是当前锁定节点f
if (tabAt(tab, i) == f) {
//说明是链表形式
if (fh >= 0) {
//记录链表节点格式
binCount = 1;
for (Node e = f;; ++binCount) {
K ek;
//链表中已经存在相同key
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
//onlyIfAbsent=true,只有在不存在该key时才会进行put操作
if (!onlyIfAbsent)
e.val = value;
//结束
break;
}
Node pred = e;
//下一个节点已经不存在
if ((e = e.next) == null) {
//直接放到下一个节点位置处
pred.next = new Node(hash, key,
value, null);
//结束
break;
}
}
}
//树形态的插入这里就不细讲了
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//当前节点的链表节点个数不是0,也就是是条链表或树形式,而不是只是单个节点在数组
if (binCount != 0) {
//链表超过8个接点就转换成树节点
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
//要插入的key与链表的key一样的旧值不为空,就返回旧值
if (oldVal != null)
return oldVal;
//结束
break;
}
}
}
addCount(1L, binCount);
return null;
}
其实整套ConcurrentHashMap难点就在扩容,数据的转移方面,所以我将单独拉出来讲:
- helpTransfer
如果当前线程添加节点的时候,发现数组正在扩容,那么当前线程就会帮助迁移,当然,帮助迁移调用的也是迁移节点的代码,代码注释很详细了,这里也就不废话了:
//关于 sizeCtl 变量:
//-1 :代表table正在初始化,其他线程应该交出CPU时间片
//-N: 表示正有N-1个线程执行扩容操作(高 16 位是 length 生成的标识符,低 16 位是扩容的线程数)
//大于 0: 如果table已经初始化,代表table容量,默认为table大小的0.75,如果还未初始化,代表需要初始化的大小
final Node[] helpTransfer(Node[] tab, Node f) {
Node[] nextTab; int sc;
//ForwardingNode 翻译过来就是正在被迁移的 Node
//关键是 hash 为 MOVED
// 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,
//就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了
//所以它其实相当于是一个标志。
//只有f的hash为MOVED,才会执行该方法,说明f节点是ForwardingNode
//如果nextTable为null,则表示迁移完成了,详见transfer()
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode)f).nextTable) != null) {
// 根据 length 得到一个标识符号
int rs = resizeStamp(tab.length);
// 如果 nextTab 没有被并发修改 且 tab 也没有被并发修改
// sizeCtl < 0 (说明还在扩容)
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 如果 sizeCtl 无符号右移 16 不等于 rs ( sc前 16 位如果不等于标识符,则标识符变化了)
// 或者 sizeCtl == rs + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,
// 当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
// 或者 sizeCtl == rs + 65535 (如果达到最大帮助线程的数量,即 65535)
// 或者转移下标正在调整 (扩容结束)
// 结束循环,返回 table
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
// 如果以上都不是, 将 sizeCtl + 1, (表示增加了一个线程帮助其扩容)
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
//进行扩容转移
transfer(tab, nextTab);
//扩容完退出循环
break;
}
}
//返回迁移的节点
return nextTab;
}
return table;
}
- transfer
旧的节点迁移到新的数组中,流程图如下:
节点迁移的代码比较难理解,以我的理解大概概括成以下几点:
- 根据系统的cpu核数,计算出每个线程的步数,也就是各自负责迁移数组长度,默认最小步长是16。
- 如果还没有创建新数组,则创建新的数组,容量是旧数组的2倍。
- 迁移节点的时候如果碰到旧数组的对应节点不存在,则直接放一个ForwardingNode(表示正在节点迁移),无需进行节点迁移。
- 如果旧数组的节点还未迁移完成,当前节点存在且当前线程获得当前节点的操作权,判断该节点是什么类型的节点,假如是链表形式,转移的时候会维护两条链表,其中一条是放置新数组的位置和旧数组一样,另一条则是旧数组位置下标加上旧数组的长度。而它是runBit属性区分要把哪个节点放置哪条链表的,runBit取值只有0和1。
- 将链表从头遍历到尾,记录最后一次与其它节点的runBit不一样的节点,并且记录这个节点,当然这样的好处在后面遍历创建节点的时候就不用再遍历记录的这个节点以及它的那些后驱节点。
- 遍历j旧的链表完毕后形成新的两条链表,将两条链表设置到新数组相应位置。
- 当然树的迁移跟链表差不多,值得一说的是,当变成两课树节点后,分别对两棵树判断树节点个数是不是小于8个,小于8个则会转换成链表,这里就不多说,源码中我都做了详细的注释。
//数据转移和扩容
//每个调用tranfer的线程会对当前旧table中[transferIndex-stride, transferIndex-1]位置的结点进行迁移
//@param tab 旧table数组
//@param nextTab 新table数组
private final void transfer(Node[] tab, Node[] nextTab) {
//stride代表步长,从后往前数
int n = tab.length, stride;
// stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16
// stride 可以理解为”步长“,有 n 个位置是需要进行迁移的,
// 将这 n 个任务分为多个任务包,每个任务包有 stride 个任务
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
//nextTab表示为新的表
// 如果 nextTab 为 null,先进行一次初始化
//外围会保证第一个发起迁移的线程调用此方法时,参数 nextTab 为 null
//之后参与迁移的线程调用此方法时,nextTab 不会为 null
if (nextTab == null) {
try {
//创建新的Node数组的容量翻倍,并作为nextTab
@SuppressWarnings("unchecked")
Node[] nt = (Node[])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 fwd = new ForwardingNode(nextTab);
// 标识一个桶的迁移工作是否完成,advance == true 表示可以进行下一个位置的迁移
boolean advance = true;
// 最后一个数据迁移的线程将该值置为true,并进行本轮扩容的收尾工作
boolean finishing = false;
// i标识桶索引, bound标识边界
for (int i = 0, bound = 0;;) {
Node f; int fh;
// 每一次自旋前的预处理,主要是定位本轮处理的桶区间
// 正常情况下,预处理完成后:i == transferIndex-1,bound == transferIndex-stride
while (advance) {
int nextIndex, nextBound;
//判断达到了bound值,或者最后一个数据搬完,advance标注false,准备退出循环
if (--i >= bound || finishing)
advance = false;
//这里 transferIndex 一旦小于等于 0,说明原数组的所有位置都有相应的线程去处理了
//标注false准备退出
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//cas计算下一个任务索引位置
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//当前是处理最后一个需要tranfer任务的线程或出现扩容冲突
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//是否迁移完成,迁移完成用nextTab替换table
if (finishing) {
nextTable = null;
table = nextTab;
//sizeCtl阈值为原来的1.5倍
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
// 然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
// 这里使用 CAS 操作对 sizeCtl 进行减 1,代表做完了属于自己的任务
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 判断当前线程是否是本轮扩容中的最后一个线程,如果不是,则直接退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
// 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
finishing = advance = true;
//回到自旋,准备退出
i = n; // recheck before commit
}
}
//旧桶本身为null,不用迁移,直接尝试放一个ForwardingNode
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 该位置处是一个 ForwardingNode,代表该位置已经迁移过了
//这里是控制并发扩容的核心
//该旧桶已经迁移完成,直接跳过
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
//该旧桶未迁移完成,进行数据迁移
else {
// 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作
synchronized (f) {
//对应的下表拿到的节点是否与当前加锁的f节点是同一个节点
if (tabAt(tab, i) == f) {
Node ln, hn;
// 头结点的 hash 值大于 0,说明是链表
if (fh >= 0) {
/**
* 下面的过程会将旧桶中的链表分成两部分:ln链和hn链
* ln链会插入到新table的槽i中,hn链会插入到新table的槽i+n中
*/
//简单说就是区分旧的节点要放在新数组什么位置,0-与旧的数组一样的位置,1-旧数组位置+n的位置
int runBit = fh & n; // 由于n是2的幂次,所以runBit要么是0,要么高位是1
Node lastRun = f; // lastRun指向最后一个相邻runBit不同的结点
//将f节点后面的节点进行遍历
for (Node p = f.next; p != null; p = p.next) {
int b = p.hash & n;
//如果下一个节点不是处于相同链表的节点
if (b != runBit) {
//runBit重新赋值,也结束runBit变为与刚才不同的值
//比如一开始0,现在变为1
runBit = b;
//记住这次不一样的节点,然后继续循环,直到没值了,记录最后一个不一样RunBit的节点
lastRun = p;
}
}
//ln链表是放新链表位置与旧链表的位置相同
if (runBit == 0) {
ln = lastRun;
hn = null;
}
//hn链表是放新链表位置=旧链表的位置+n(旧数组长度)
else {
hn = lastRun;
ln = null;
}
//因为lastRun和它后面的节点已经赋值给ln或hn,则这里就不用遍历了
for (Node 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(ph, pk, pv, ln);
else
hn = new Node(ph, pk, pv, hn);
}
// 其中的一个链表放在新数组的位置 i
setTabAt(nextTab, i, ln);
// 另一个链表放在新数组的位置 i+n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
// 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance 设置为 true,代表该位置已经迁移完毕
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin t = (TreeBin)f;
//也是维护了两棵树lo,hi
TreeNode lo = null, loTail = null;
TreeNode hi = null, hiTail = null;
int lc = 0, hc = 0;
//遍历树节点
for (Node e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode p = new TreeNode
(h, e.key, e.val, null, null);
//runBit==0则节点放lo
if ((h & n) == 0) {
//当前节点的前驱节点,也就是loTail记录的尾结点
if ((p.prev = loTail) == null)
//放在低位链表
lo = p;
//尾结点有值,就将尾结点的next指针指向当前节点
else
loTail.next = p;
//更新尾结点
loTail = p;
//低位节点统计
++lc;
}
//runBit==1则节点放hi
else {
////当前节点的前驱节点,也就是hiTail记录的尾结点
if ((p.prev = hiTail) == null)
//放在高位链表
hi = p;
//尾结点有值,就将尾结点的next指针指向当前节点
else
hiTail.next = p;
//更新尾结点
hiTail = p;
//高位节点统计
++hc;
}
}
// 如果一分为二后,节点数少于 8,那么将红黑树转换回链表
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin(lo) : t;
// 如果一分为二后,节点数少于 8,那么将红黑树转换回链表
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin(hi) : t;
// 将 ln 放置在新数组的位置 i
setTabAt(nextTab, i, ln);
// 将 hn 放置在新数组的位置 i+n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
// 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance 设置为 true,代表该位置已经迁移完毕
advance = true;
}
}
}
}
}
}
- treeifyBin
我们还是先看下流程图:
由链表转成红黑树结构大概概括以下几点:
- 如果数组的长度小于最小数组长度64的话则进行扩容,而不进行红黑树化,扩容为原来长度的2的n次方倍。
- 如果是链表结构并且个数超过8,则进行红黑树化,详细看我源代码注释。
//当数组长度小于64的时候,扩张数组长度一倍,否则的话把链表转为树
private final void treeifyBin(Node[] tab, int index) {
Node b; int n, sc;
//数组不为空
if (tab != null) {
//数组的长度小于64的话,这是约束
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
//尝试调整表的大小以适应给定的元素数量,扩容为原来的2的n次方倍
tryPresize(n << 1);
//找到下角标为index的节点,不为空,并且hash大于0,说明是链表形式的,下面变成树
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
//双重确认,再次确认index节点下的节点是不是跟刚取出来的b是同个节点,防止并发
if (tabAt(tab, index) == b) {
TreeNode hd = null, tl = null;
//遍历链表
for (Node e = b; e != null; e = e.next) {
//创建树节点
TreeNode p =
new TreeNode(e.hash, e.key, e.val,
null, null);
//如果当前树节点没有前驱节点,将当前节点给hd头结点,也就是p自己成为头结点
if ((p.prev = tl) == null)
hd = p;
//如果当前树节点有前驱节点,并且是tl尾结点,则将tl的next指针指向p,让p成为tl后驱节点
else
tl.next = p;
tl = p;
}
//整棵树设置进数组的index处,完成链表到树的转换
setTabAt(tab, index, new TreeBin(hd));
}
}
}
}
}
- tryPresize
//将数组扩容为原来的2的n次方倍,size参数传进来的时候是n << 1,也就是已经变成原来的两倍
public final void tryPresize(int size) {
//size在传入之前就已经翻倍了,最终c是一个大于等于(size * 1.5 + 1)的2的幂次方数
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
//此时的sizeCtl是cap * 0.75,扩容阈值
while ((sc = sizeCtl) >= 0) {
Node[] 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[] nt = (Node[])new Node,?>[n];
table = nt;
//n默认16,将sc也就是后面要给sizeCtl阈值变为12,也就是原来大小的0.75
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
//一直扩容到的c小于等于sizeCtl或者数组长度大于最大长度的时候,则退出
//所以在一次扩容之后,不是原来长度的两倍,而是2的n次方倍
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
//table不为空,且在此期间其他线程未修改table
else if (tab == table) {
int rs = resizeStamp(n);
//数组正在扩容,因为sc<0
if (sc < 0) {
Node[] nt;
// 如果 sizeCtl 无符号右移 16 不等于 rs ( sc前 16 位如果不等于标识符,则标识符变化了),数组数据迁移还没结束
// 或者 sizeCtl == rs + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,
// 当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
// 或者 sizeCtl == rs + 65535 (如果达到最大帮助线程的数量,即 65535)
// 或者转移下标正在调整 (扩容结束)
// 结束循环,返回 table
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//cas比较尝试将sc加一,代表当前线程帮助数组扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
//迁移数据
transfer(tab, nt);
}
//到这里说明没有线程在扩容,没有线程在迁移数据,所以cas设置比较sc的值
//默认第一个线程设置 sc ==rs 左移 16 位 + 2,
//当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//迁移数据
transfer(tab, null);
}
}
}
get方法分析
以下是流程图:
看了上面的put方法解析,看get会容易很多,因为难点都在put,简单概括几点:
- 数组存在且找到相应的节点与要找的key一样,则直接返回该节点的值。
- 如果找到节点正在迁移或者是树节点,则用ForwardingNode的查找方法对新数组进行查找或者用TreeNode的查找方法进行查找。
- 如果是链表节点则遍历查找即可。
public V get(Object key) {
Node[] tab; Node e, p; int n, eh; K ek;
int h = spread(key.hashCode());
//找到数组相应位置的节点,并且节点的key和当前key想的 则直接返回
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;
}
//数组正在扩容,调用ForwardingNode的find去查找,当搬一个旧的节点到新数组的时候,就会在
//旧的数组该节点出设置为ForwardingNode
//这里有可能调用TreeNode的find方法,TreeNode=-2,ForwardingNode=-1
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//到这里 节点是链表形式的,开始遍历比对key返回
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方法
static final class ForwardingNode extends Node {
//新的数组
final Node[] nextTable;
ForwardingNode(Node[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
Node find(int h, Object k) {
//tab是搬迁后的新数组
outer: for (Node[] tab = nextTable;;) {
Node e; int n;
//新数组为空或者在新数组中没有找到
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
//到这块就是在新数组里已经定位到
//自旋比对key,找到对应的key
for (;;) {
int eh; K ek;
//如果key相同就返回相应节点,结束
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
//如果该节点正在搬迁,或者是树节点,-1是ForwardingNode,-2是TreeNode
if (eh < 0) {
//如果是正在搬迁的节点,将它维护的数组重新对tab赋值,回到for循环,也就是更新tab,
//因为正在搬迁,所以新数组里面的节点都是处于不断更新的状态
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode)e).nextTable;
continue outer;
}
//该节点是树节点,则调用TreeNode的查找方法,按树的格式查找节点
else
return e.find(h, k);
}
//查找下一个节点
if ((e = e.next) == null)
return null;
}
}
}
}
remove方法分析
简单地做一下概括:
- 要删除的节点不存在则不进行任何操作,直接返回空。
- 如果要删除的节点正在迁移,则当前线程先去帮助迁移,后续再进行相应的删除操作。
- 不管是树节点还是链表删除都是对其进行遍历,然后将指针的引用
public V remove(Object key) {
return replaceNode(key, null, null);
}
- replaceNode
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
//自旋
for (Node[] tab = table;;) {
Node f; int n, i, fh;
//数组为空或者数组下标对应的节点不存在,就直接跳出自旋,返回空
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
//如果该节点正在迁移数据,当前线程帮助迁移
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//存在节点,且节点没有处于搬迁状态
else {
V oldVal = null;
//校验标识,只要获得锁,进入链表节点或者树节点,都会更改为true
boolean validated = false;
//锁定头结点
synchronized (f) {
//再次确认数组当前索引下的节点是否变更,没变更则开始进行相应的操作
if (tabAt(tab, i) == f) {
//链表形式
if (fh >= 0) {
//校验标识设为true
validated = true;
//这个过程是遍历当前头结点下的所有节点,直到找到与我们当前要删除的key一样的key,
//然后修改相应头尾指针和设置数组新的头结点等等
for (Node e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
//该节点是红黑树节点,跟链表差不多,这里就不做过多解释
else if (f instanceof TreeBin) {
validated = true;
TreeBin t = (TreeBin)f;
TreeNode r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
//删除了节点
if (validated) {
if (oldVal != null) {
//value是参数,它必为空,则进行
//增加计数,如果表太小且尚未调整大小,则开始数据迁移。
//如果已经调整大小,则在工作可用时帮助执行转移。
//转移后重新检查占用率,以查看是否已经需要其他调整大小,因为调整大小是滞后的。
if (value == null)
addCount(-1L, -1);
//返回删除后的节点值
return oldVal;
}
break;
}
}
}
return null;
}