文章目录
前言
1.8的ConcurrentHashMap相比于1.7可以说发生了相当大的变化,虽然添加了红黑树的数据结构,但是整个table的数据结构确实完全简化了。 另外加锁的实现也再不像DougLea大师那样无限的使用UNSAFE。 但是扩容增加了**帮助扩容**的实现,是本类最难的地方。一、数据结构
CurrentHashMap主要的数据结构是Node[], 简化了不少。
而另外两个主要的类型是TreeNode和TreeBin【是判别Node和红黑树和加锁的重点】,都继承了Node类。
通过hash值来区分TreeBin与普通Node
几个属性:
table[]:主要数据存储单元的指针数组
baseCount:类似size,只不过有一些多线程的行为
sizeCtl:一个标志位,-1表示当前已经有其他线程在执行初始化或者扩容;0表示table等待一个线程帮他初始化或者扩容;>0,表示当前已经初始化完成的table大小
countCells[]:CountCell对象数组,这是一个多线程共同访问的节点个数计数器,当其他线程获得了baseCount的修改权利之后,没抢到cpu的线程会争夺countCells修改权利,之后修改值传到baseCount
transIndex:表示现在还差多少节点的扩容任务没有分配,当前线程可以承担。
1. Node
Node类是链表节点类,具有老一套的四个属性。【val竟然简化了。。。】
和HashMap一样,他的hashCode计算方式仍然是key.hashCode() + val.hashCode()
equals()要求两个Node必须key,val不为空且相等。
find()就是简单的链表遍历。
2. Segment
与1.7不同,Segment大权旁落,基本没几个属性,看注释的意义是只有序列化的时候有用。
3. TreeNode
作为红黑树的节点,他拥有树的相关的指针:parent,left,right,以及属性red
也有Node类的next与prev.
在HashMap中已经很熟悉他了。
不同点在于,HashMap中的TreeNode与Node共同继承Map.Entry类,而此时Node继承Entry,而TreeNode继承Node
Node中设计有key与value的属性也一并继承给他了。
4. TreeBin
Bin是桶子的意思。
TreeBin是封装TreeNode头指针的数据结构。【即类似1.7Segment的作用,但是内部不再是一个小的HashMap,而是类似1.8中一个普通table[]的单元格,保存一个红黑树头部】
根据注释,他不保存乐乐好键-值,而是只有指向TreeNode的root节点的指针,即first。同时,它具有锁的能力,加锁后强制要求其他线程等待。
提供lockState属性表示当前自己的线程所处的状态。
TreeBin的存在意义很大:
我们知道红黑树经常有旋转的行为,常常根节点也会发生旋转。
若其中某一时刻,某个线程将根节点旋转,还未来得及修改table[i]的指向,这时其他线程可以得到这个已经上锁的“假root”,会造成意外的错误。【锁变化】
构造方法
TreeBin(TreeNode<K,V> b) {
//Node(int hash, K key, V val, Node<K,V> next)
super(TREEBIN, null, null, null);
this.first = b;
TreeNode<K,V> r = null;
for (TreeNode<K,V> x = b, next; x != null; x = next) { //依次处理每个结点
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (r == null) {
x.parent = null;
x.red = false; //根结点为黑色
r = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = r;;) { //遍历查找新结点存放位置
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
//key有实现Comparable接口则使用compareTo()进行比较,否则采用tieBreakOrder中默认的比较方式,即比较hashCode。
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) { //左子节点或右子节点为空则在p下添加新结点,否则p的值更新为子节点继续查找。红黑树中结点p.left <= p <= p.right
x.parent = xp; //保存新结点的父结点
if (dir <= 0)
xp.left = x; //排序小的放左边
else
xp.right = x; //排序大的放右边
r = balanceInsertion(r, x); //平衡红黑树
break;
...
this.root = r;
...
}
构造方法责任重大,负责红黑树的生成工作。
first:记录插入的节点
x:工作指针
next:x.next
r:即root节点【一般第一次看到的节点就是根节点,想象一下,现在进入TreeBin的里世界,是不是第一个就是root?】
dir:记录比较结果,类似compare()的结果记法
- 第一个root的节点的颜色变成黑色,将r记录根节点
- 类似HashMap的逻辑,经历四重大小判断,直到不相等或者全部相等为止。
- key值的hash(int类型)比较
- Comparable.compareTo()【这两个方法前者获取Comparable类对象,若获取成功,说明key是Compable类,第二个方法就直接调用compareTo()】
- tieBreakOrder()与HashMap一样,拥有两个比较功能:
- 比较类型名称【String比较】
- 比较原生hashCode()
- 根据比较结果,选择走哪个子树【二叉搜索树查找规则】
- 为TreeBin类成员变量root赋值为r
5. ForwardingNode
这是一个临时的树节点,用于在扩容时替换已经transfer过的节点,其他线程操作这个节点也会过来帮助扩容
二、方法
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//不允许NULL的键和值,因为并发情况下无法分辨是不存在Key还是没有找到,
//以及Value本身就为空,所以不允许NULL的存在;HashMap可以判断,因为containsKey
//方法存在。而在多线程中,contains后去get,可能会发生修改或者删除,无法判断;
if (key == null || value == null) throw new NullPointerException();
//计算Hash值
int hash = spread(key.hashCode());
int binCount = 0;
//死循环, 可以CAS不断竞争,或者协助扩容后出来继续干活等等;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果未进行初始化,则初始化;接下来分析;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果目标桶位为空,则通过CAS插入;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//当第一个元素的Hash值为MOVED(-1),代表为ForWardingNode,正在扩容
else if ((fh = f.hash) == MOVED)
//则该线程协助扩容;
tab = helpTransfer(tab, f);
else {
//无事发生,我们继续synchronized加锁后慢慢进行传统添加Node
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
//fh>=0,代表我是链表
if (fh >= 0) {
binCount = 1;
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, null);
break;
}
}
}
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
put()就直接调用一个API。
1. putVal()
这个方法的注释说:是put()以及putIfAbsent()的具体实现。
-
多线程环境不能保证前一刻得到的节点此时还存在,因此不提供空的key/value
若未空,直接抛出异常NPE -
调用spread()获取当前插入节点的key对应的hash
spread():
类似HashMap的操作,将高16位于低16为进行异或,不同的是,还与了一个参数,查看发现是一个除了最高位为全一的数,注释说是普通节点hash的可用位。 -
binCount:记录当前数组下标下的节点个数,用于链·树转换
-
进入循环:
f:表示数组对应位置的首节点【可能是链表,也有可能是红黑树】
fh:表示节点的状态
n:链表长度- 若tab没有初始化,调用初始化方法【initTable后面说】
- 若该TreeBin为空,通过cas操作插入赋值新节点,终止循环
很熟悉的做法:U是Unsafe对象,左移ASHIFT代替* (sacle = arrayIndexSacle(Node[].class))
的行为
【ASHIFT == 31 - Integer.numbersOfLeadingZeros(ak)
ABASE = U.arrayBaseOffset(ak);
】ak == Node[].class
ABASE表示基准地址,i << ASHIFT表示偏移量,这句话表示取出Node[]的第i位的对象数据出来。【cas使用操作系统命令cas读取主内存信息,由于对总线加锁,因此同一个时刻只有一个线程可以读取、修改】
- 此时进入真正的插入语句:
对当前的TreeBin加锁:【很直接,直接来synchronized】- 第一句的if判断很莫名奇妙,其实很有深意【在加锁的时候,可能有其他线程修改删除了这个TreeBin对象,因此要加一次判断】
- 若fh < 0表示当前正在转化树中,当前线程也会去参与搬运工作【helpTransfer()之后看】
- fh之前赋值了f.hash,趁现在查看几个hash常数:
moved表示当前TreeBin内正在搬运节点
treebin表示当前节点指向树根
reserved表示临时保留的哈希
这几个都不是正常hash,全都小于0
- 因此此时hash>=0,表示为正常链表节点,进行循环查找。找到后,进行替换【onlyIfAbsent一旦设为true,表示当前键值对的key值对应的节点已经存在了,就不会进行覆盖,普通put()默认为false】,记录原值,跳出循环
- 若找到,说明这个键值对对应的节点不存在,因此直接new一个Node挂在最后【1.8的CHM采用尾插法】
- 若当前数组的头Node节点为TreeBin,表示下面是红黑树,调用putTreeVal()进行替换,方法返回被替换节点,是否替换由当前方法决定【putTreeVal()之后说】
- 若binCount大于树化的临界值(8),进行树化
树化的逻辑很简单:
加锁操作【之前调用的那段代码未加锁,这里加了锁】
若为超过设定的table阈值,不会进行树化,而是优先选择扩容一倍。
若超过,将所有的普通链表Node更换为TreeNode,并将这个TreeNode的首节点赋值给table中index的指针。【并不是变成树,而是变成双向链表,在构造方法中,有红黑树的生成逻辑】《这里选择了构造方法创建对象》 - 进行addCount(),应该是count加一,之后可能会看。
2. initTable()
/**
* Initializes table, using the size recorded in sizeCtl.
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//当前表为空,我们尝试当老大,把表初始化(单押)
while ((tab = table) == null || tab.length == 0) {
//小于0,我们在竞争中失败了,睡一觉等着别人初始化,初始化时sizeCtl为-1;
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//好像还没人过来,我们赶紧通过CAS将sizeCtl置-1,慢慢初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//老子是第一个,开始初始化,完成后将sizeCtl设置为扩容阈值;
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 - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
- 只有table未被初始化,才能进行逻辑:
- 将sc赋值sizeCtl【开头提过作用】,判断此时sc的值:
- 为-1,其他线程正在初始化,当前线程放弃cpu,重新争夺CPU【有可能这个线程马上复活,继续走if,再次不成功,继续放弃,这样就是一个while(true)类似的逻辑,可能造成cpu占用率过高。这是jdk1.8的一个问题】
- 进行cas,修改sc值为-1,若成功,表名当前线程争夺到了初始化table的能力s,进行一系列赋值【sc的值为何赋值为
n - n >>> 2
?】
- 将sizeCtl修改为sc的值,返回tab
- 将sc赋值sizeCtl【开头提过作用】,判断此时sc的值:
3. putTreeVal()
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
else if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
TreeNode<K,V> x, f = first;
first = x = new TreeNode<K,V>(h, k, v, f, xp);
if (f != null)
f.prev = x;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
if (!xp.red)
x.red = true;
else {
lockRoot();
try {
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
}
break;
}
}
assert checkInvariants(root);
return null;
}
和HashMap和本类的TreeBin构造方法的红黑树查找插入节点一个原理:
第一步,寻找插入位置【若存在相同键值对,不进行插入】
先进行比较,再决定插入左子树还是右子树,若key相等就修改【key是唯一的,但是hash可能不是唯一的】
若hash相等,执行其他三重比较。
serched是用来标识是否将当前子树拉入递归的标识位,若递归返回不为空,表名查找到了与插入节点的键值对完全相同的节点,无需插入,直接返回该节点。
balanceINsertion因为有锁的存在,因此无需加锁,与Has和Map的基本一致。
若未找到,开启插入逻辑。
新建一个TreeNode节点,赋值该节点的各个指针。
加锁,插入,并调整红黑树:balanceInsetion():和HashMap基本相同,不再赘述
4. addCount() 开启扩容方法
这个方法分为两个部分:修改baseCount;查看是否需要扩容
这个方法的技巧很值得学习:
采用了几个或短路的操作进行赋值,超帅!
//添加计数,如果表太小而且尚未调整大小,则启动扩容。 如果已经调整大小,则在工作可用时帮助
//执行扩容。 在扩容后重新检查占用情况,看是否需要继续扩容。
//从 putVal 传入的参数是 1, binCount,binCount 是链表的长度/红黑树的结点数
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
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)
return;
s = sumCount();
}
// 检查是否需要扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//表的长度大于sizeCtl(阈值),且表不为空,表的长度小于最大值,则开始扩容
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
//正在扩容
if (sc < 0) {
// sizeCtl 变化了
//扩容结束;第一个线程设置 sc ==rs 左移 16 位 + 2,当线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1
//达到最大扩容线程数
//扩容结束,则nextTable为空
//任务已被全部分配
//这么多情况下,我不需要帮助扩容
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//如果可以帮助扩容,那么将 sc 加 1. 表示多了一个线程在帮助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//没有在扩容,由我开启扩容状态,标识符左移 16 位 + 2. 也就是变成一个负数。高 16 位是标识符,低 16 位初始是 2.
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
//统计元素数量
s = sumCount();
}
}
}
x表示当前线程增加的节点数量
check:
- 得到成员变量counterCell()赋值给as【若为第一次调用,counterCells必定为null】
- 第一次或短路:若as为null,直接跳过cms的操作【巧妇难为无米之炊】
- 若不为null,进行cas,试图修改baseCount,完成自己的任务
- 第二个牛逼的操作:在进行cas的同时,进行判断,若cas成功,无需竞争counterCells,若未成功,且counterCell不为空才会进入竞争counterCells【findAddCount()具有初始化、竞争cellCounters的单元,并一定将x加到baseCount或者counterCells任一个单元格的作用,下面会说。】
- uncontent:
- 接下来又是好几个或短路:
- 再次进行判空,防止之前几句执行中其他线程删除了as
- m赋值为as的长度,m == 0也是没有初始化
- 接下来需要分析一下:
ThreadLocalRandom.getProbe()的作用是生成一个随机数,你可以认为生成了一个“假的哈希值”,然后与as.length - 1进行与运算【是不是很熟悉,没错,就是在算哈希表数组的下标】a == null,说明as数组这个位置还没有初始化;执行到第四个条件判断,说明as的下标位置已经初始化,进行cas修改该as数组的值,若修改成功将直接退出
执行到这里,感觉到了这个作者的功力:
条件是层层递进的,而前面如果不成立后面没有执行的必要,要是我写会多写几个if(),大师就在一个if的条件里面就已经完成求下标、求长度、cas赋值等等操作,赏心悦目,流畅如斯!
进入if块中,说明as数组没有初始化好,退出进行初始化;
- check <= 1
- sumCount()方法是一个很简单的逻辑,将counterCells数组进行遍历,将每个counterCell的count值加到baseCount中:
使用counterCells[]代替多线程争抢着去修改baseCount是一种fork/join行为,可以增加多线程的下的执行效率。【fork指退而求其次【分解问题】,join:将求其次的结果放到最初想修改的结果中】
下半部分是扩容逻辑。
标志位:
-
sc:需要扩容还是帮助扩容;
-
rc:用于与sc进行比较,得到当前扩充线程的数量
-
transferIndex:还剩下的扩容任务数量【原来是数组长度,一个线程取掉几个就会减几个】
-
nextTable:存储新数组
-
检查check,若大于开始进行扩容条件判断:
-
s = baseCount + x,即加上增加的节点数后,必须比表长要长;原表不为空;且当前长度没有超过最大长度 1 << 30,即2的29次方。
- resizeStamp():注释说是用来控制传入的n的大小,若利用这个方法将表长n左移必定为负【原理不甚明白,因此是整形溢出。这个计算太复杂,以后再搞】
+ sc < 0,表示当前已经有线程在扩容了,当前线程进行判断,考虑是否帮助扩容,只要满足一个,就不进行扩容,条件有:
+ sc右移后不等于rs,表示sizeCtl发生了变化;
+ 扩容结束:扩容最后,sc会设置为rc + 1;newTable为空【因为扩容后,这个暂存数组的变量会被赋值为空。】
+ transferIndex <= 0, 所有位置的扩容已经有其他线程承担了。
+ 若可以扩容,将sc + 1,表示当前多了一个线程来帮助扩容。
+ 若sc >= 0,表示当前无人扩容,修改标志位sc,修改为rc << 18位,标志着开始扩容,扩容方法请看transfer
5. fullAddCount():务必完成size增加操作方法【附带有counterCells的初始化及扩容】
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
//如果当前线程hash值==0 就执行下,具体目的还不清楚
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
//循环
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
//如果counterCells已经被初始化了
if ((as = counterCells) != null && (n = as.length) > 0) {
//如果当前线程对应于counterCell数组中的槽位为空,在此位置添加一个CounterCell元素
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
//wasUncontended一直为true
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//如果当前线程对应槽位已经存在CounterCell元素了,就对value+x
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
//为扩容做条件
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
//扩展数组,长度变为两倍
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
//如果counterCells 没有被初始化,
//(由上面可知cellsBusy是用来在初始化和赋值扩容时做判断的)
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
//初始化长度为2
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
//如果都不满足 最后还是cas当前map对象 baseCount + x
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
h:通过ThreadLocalRandom类生成的“伪哈希值”
collide:counterCells[]扩容标记位
as:counterCell[]
n:as.length
cellBusy:as是否在进行扩容和初始化的标记位【注意,这个标志位一旦为1,其他线程不能再操作该数组任何单元格!】
a:当前线程通过hash计算得到的对应as数祖下标处的CounterCell对象
wasUncontended:让当前这个线程在试图扩容之前再考虑进行一次hash
created:当前cellCounters[]线程试图创建的这个单元格是否创建成功
- 第一个if:若哈希值为0,说明ThreadLoaclRandom类没有初始化,进行初始化并获取一个hash值
- 进入循环:【大量的if】
- 第一个if:若as已经初始化,才能进入争抢as的逻辑:
- 若当前线程计算得到的位置,该单元格的CounterCell对象还没有被初始化,才会进入这个if;若cellsBusy == 0,说明没有线程在操作这个单元格,此时当前线程给这个单元格初始化:【说错了,cellsBusy是整个数组的标志位,而不仅仅是一个单元格的】
- 制造一个CounterCell对象,利用cas方法将CellBusy标志位置为1,告诉其他线程不要来和他强这个单元格的操作权,并设置created标志位用于跳出循环的创建成功检查
- 此时再次判断这个单元格是否为空【防止在前的几句代码执行时,其他线程已经修改了counterCells[]数组】,若成功,将自己制作的cell对象给这个数组位置引用上,并将标志created置为true,表示成功创建;
- 将cellBusy = 0,表示自己放弃了这个单元格的操作权,其他线程可以来争夺;
- 检查created位,若为true表示这趟循环已经完成了创建,跳出此时循环,再次进入其他的逻辑。
- 若当前线程计算得到的位置,该单元格的CounterCell对象还没有被初始化,才会进入这个if;若cellsBusy == 0,说明没有线程在操作这个单元格,此时当前线程给这个单元格初始化:【说错了,cellsBusy是整个数组的标志位,而不仅仅是一个单元格的】
- 第一个if:若as已经初始化,才能进入争抢as的逻辑:
这个方法经常采用标记位 + finally的方式,值的学习
+ 看清楚collide设置的位置,处于cellBusy的检查外面,说明这个线程争抢单元格没有成功,因此接下来可能考虑扩容;
+ 修改wasUncontened【相当于浪费一次循环,在下次循环前会跳出这个esle语句块,执行一次hash,再重新看看能不能找个别的坑占了,先不着急扩容】
+ 执行cas,将对应位置的CounterCell对象尝试修改值,若修改成功,就结束循环,完成任务;
+ 下个if是禁止counterCells在做扩容的语句:当countercells数组的大小已经到达CPU的最大核心数,不会再进行扩容;【因为是if-else if的操作,所以只会执行一个,即使上一轮下面那个else if将collide设置为true,表名下次循环即将执行扩容,但是仍然被和else if给截胡了,扩容不会进行】
+ 这个else if告知下次循环即将进行扩容【collide == true】
+ 若n未达到cpu最大核心数,上面两个else if都不会执行,进入扩容的逻辑:
+ 首先争夺标志位,标明当前线程对于这个单元格的控制权
+ 将CounterCells[]容量增加一倍,并直接将老数组的对应下标元素复制过去【没有rehash的行为了】,修改成员变量
+ 结束循环,collide仍然设置为false【表明的意思是自己这次还是没有完成任务,若下次再完不成,还是选择扩容】
collide的含义是,若前一次的争夺单元格未成功,且第二次的争夺也未成功【若成功,不会进入到扩容逻辑】,说明这个数组单元格不足【 其实不是,因为可能只是他比较倒霉,两次hash算出得到下标都是被人占用的位置,还有空位置他没得到】
+ 再计算一次hash,重新进行尝试,若尝试失败,还是会进行扩容
- 下面的else if就是另外的逻辑了——初始化【之前开始的if判断的是countersCells是否等于null,已经length是否为0,进入下面这个分支,说明未得到初始化】
- 先利用cas得到整个数组的操作权,防止初始化出现以外
- 一样的设置创建标志位,在finally中返还cellsBusy,在后面检查创建标志位,成功直接结束任务;
- 在if中,初始化数组大小为2,此时“与操作”掩码为1,因此计算下标直接与1.
- 因为这是这个线程创建的CounterCells[]自然有权利先试用,近水楼台先得月,直接把直接的x附上去了事
【这样的做法,造成了两个后果:
- 这个线程完成数组初始化的同时也就完成了加size的任务
- 其他线程使用这个couterCells数组时,这个数组已经至少拥有一个单元格被初始化】
+ 若这个线程倒霉到连初始化都没得机会,只好再去尝试cas 原来的东家baseCounter,反正都是一样的。
这两个位置其实本质都是一样,意义都是叫当前这个没竞争成功的线程再进行一次hash,再重新竞争,而不是着急扩容,因为真的可能出现倒霉到hash好几次老是占不到坑,却偏偏有坑没占到的情况。
6. transfer()扩容方法
扩容时从右向左进行的。
tranferIndex表示当前还未转移的节点数量,默认为原表长,每次扩容之前都会直接减掉这一趟扩容的节点数量。
i表示右边界,有丶像工作指针,每次转移任务后,i向左移一位;
bound表示左边界,一旦这个区间为0代表一次转移任务完成。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//算每条线程处理的桶个数,每条线程处理的桶数量一样;
//如果CPU为单核,则使用一条线程处理所有桶;毕竟可能出现帮助扩容,大家不能越界
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,他的Hash值为-1(MOVED)
//作用是告诉大家这个表正在扩容,快来帮忙;
//以及,查询的时候看到我,指向了下一个表,你去那里看看;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
//循环处理一个stride长度的任务,i后面会被赋值为该 stride 内最大的下标,而
//bound 后面会被赋值为该 stride 内左边界;通过循环不断减小i的值,从右往
//左依次迁移桶上面的数据,直到i小于bound时结束该次长度为 stride 的迁移任务
//结束这次的任务后会通过外层 addCount、helpTransfer、tryPresize 方法的
// while 循环达到继续领取其他任务或者没有未分配的任务区间就休息;
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
//处理一个桶就i减1,进行
if (--i >= bound || finishing)
advance = false;
//transferIndex<=0证明任务分配完毕,i置-1,advance为false,后续根据这个退出扩容
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//首次进入for循环会进入该函数,设置任务区间
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//扩容结束,nextTable只有扩容时才不为null;将table指向新表,重新设置sizeCtl
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//每当一条线程扩容结束就会更新一次 sizeCtl 的值,进行减1操作,扩容中,sizeCtl表示有多少个线程
//正在扩容;
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//不是最晚一个干完活的,不用关灯
// 第一个扩容时候设置了U.compareAndSwapInt(this, SIZECTL, sc,
//(rs << RESIZE_STAMP_SHIFT) + 2)
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//最晚离开的,将i设置为n,再重新检查是不是所有的结点都完成转移了
finishing = advance = true;
i = n; // recheck before commit
}
}
//空桶,放fwd标识扩容状态;
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//已经放置了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) {
int runBit = fh & n;
Node<K,V> lastRun = f;
//解释一下lastRun,就是最后的连续N个相同的Node,我们需要将当前桶的元素根据
//前一位Hash值分到第i个桶和第i+n个桶上;那么lastRun代表的就是,最后连续的
//多个相同目标桶的的Node链表的第一个Node
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
//根据runBit,确定是放到第i个桶还是第i+n个;
//LastRun结点后直接迁移,是修改指针,lastRun作为头结点
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
//除了LastRun结点,其他结点采用复制,倒序插入
//(倒序的原因是后插入的结点被访问的可能性更大)
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)
//next指向原来的ln,并ln引用指向自己,实现倒序;
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//setTabAt方法调用的是 Unsafe 类的 putObjectVolatile 方法,将ln和hn挂到nextTable上;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
//给原table设置fwd,标识迁移完成;
setTabAt(tab, i, fwd);
advance = true;
}
//同上应该差不多,立个flag,等我学会了红黑树我就回来写
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;
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;
}
}
}
}
}
}
变量:
- n : oldTab的表长;
- stride:扩容步长【一次扩容的节点数量】
- nextTab:全局变量,代表初始化的新数组;引用为nt
- transferIndex:还需扩容的节点数
- nextn:新表长度
- fwd:临时替换节点
- advance:是否继续扩容。用于给扩容语句查找下一个扩容区间,一旦找到,设置为false,执行扩容
- finishing:是否完成扩容
- i:扩容右边界
- bound:扩容左边界
- nextIndex:下一轮右边界
- nextBound:下一轮左边界
- 计算步长stride:stride与cpu的核心数有关,只有多线程情况下赋值为表长 / cpu核心数【很好理解,相当于若所有cpu核心都在帮助扩容,那就每个线程平分来进行认务】;否则设置为单线程任务量
- nextTab只会在扩容中有值,扩容结束后为空,因此判断他为空可以说明当前未开启扩容。
- 创建新表,表长为原来大小的两倍。
- 设置sizeCtl为最大值,用于计算参与扩容的线程数量。
- 初始化transferIndex为老表长
- 初始化一个ForwardingNode的node节点,用于之后替换原table的node【原理是将hash设为-1,在之前的put中有过,当hash < 0,就过来helpTransfer()】
- 初始化扩容边界
- 当advance标志为true,表明还需要进行扩容
- 从右向左,若i 已经移动到了bound的左边,说明[bound, i]的区间已经不存在,扩容完毕【finishing == true也是】,跳出扩容循环
- 当transferIndex <= 0,说明任务分配完毕,无需参与扩容
- 在下一个else if中,初始化下次扩容的左边界:nextBound, 只要扩容区间还能大于一个步长,就算出正常的左边界,若不能大于,直接将nextBound设为0,使得必定从第一个if跳出。此时是一个cms操作,这个赋值未必可以成功,看接下来的代码
- 迭代i和bound,advance设为false,跳出当前while循环
- 接下来的几个条件都说明i已经超出了原来表的范围,即扩容结束了,将全局变量设置一下,结束程序【i之所以会大于等于n,是因为下面的那个if中的结束操作会将i赋值为n】
- cms操作sc,将sc减一;因为初始时sc设置为:rs << RESIZE + 2,且每个线程进入helpTransfer()时都会将sc + 1,且在这里执行cas减一,因此若此时也等于这个值,则说明当前就是最后一个线程了,将finishing置为true,i设为n,表示循环结束【之所以advance也设置为true,是为了能够保证在下次循环时还能进入那个while循环,之后按照条件赋值退出【就是上一个if finishing中的全局变量赋值】】
- 若advance为false,且当前没有扩容结束,利用cas取出要操作的那个桶位【以前一直叫数组下标的位置,老长了,感觉这个名字比较装逼】,若取出成功,且该桶位为空,就替换为transfer的标志节点fwd,若赋值成功,advance设为true
casTabAt()表示对tab这个备操作对象,当tab[i] == null时,将其赋值为fwd
- 若该桶位不为空,且放置了fwd,说明已经有其他线程transfer过了,advance设为true;
- else就代表桶位不为空,且其中的元素还是普通的Node没有经过搬迁,于是当前线程帮助搬迁:
- 对该桶位上锁,执行扩容逻辑:
fh > 0:根据刚开头的分析,为了区分节点,他将TreeBin的hash值设为负数,而此时fh > 0,说明是普通链表节点的插入逻辑:
- 第一步还是检查上锁的时候目标有没有被修改;
- 几个变量:
runBit:和HashMap原理一样,若是当前桶位对应的hash & oldTab.length[在这里,n就是旧表长] == 1,就搬迁到 i + n的下标处;若 == 0,就搬迁到i的下标处。而runBit就是这个运算结果。
lastRun:表示一个小链表的头结点,后台jdk1.7的ConcurrentHashMap一样,用来存储一组相同的下标一组相邻的链表节点【比如说,这个桶位上的链表,其从第二个到第四个都是装到新链表的第i个位置,那么这几个Node都会挂到同一个lastRun下面,之后统一转配过去】
p:工作指针,用来遍历当前查找到的新的不同转移下标【这么形容好难,统一记为index吧,取值只有i与i+n】的节点,比如说上一轮已经解决了几个相邻的转到i下标节点,此时lastRun就代表后面那个i + n的节点,此时将lastRun指针暂停,用p来探勘lastRun后面与lastRun一样都是会转移到i + n的几个相邻节点,直到不是转移到 i + n为止。
b:就是p计算的下标,与lastRun的下标进行比较
ln:存储需要转移到下标i的几个连续节点
hn:存储需要转移到下标i + n的几个连续节点
感觉上面介绍变量就已经吧所有的逻辑写了,就不赘述了。
额外提几个:
- 当b != runBit时,发生将lastRun变成下一轮的lastRun
- 虽然之前说是“挂”到lastRun后面,但其实因为她们几个本来就是相连的,根本不需要更改指针,唯一要做的就是把这lastRun赋值给ln或者hn, 在之后的逻辑中插入新数组即可。【因为他的插入法是,一次插入同一下标几个元素】
上一篇HashMap已经分析了当前采取的转移策略有哪些,而一次插几个相同下标是jdk1.7的逻辑,1.8的ConcurrentHashMap也采用了。
接下来是复制节点逻辑:将原节点深拷贝到新节点上,并挂到hn与ln指针上【采取的方式是头插入,可以看到,每次都将next赋值前一个头结点,然后再迭代头指针。】
将这个链表挂到tab的相应桶位置,采取的方式依然是头插法。
该桶位移动完毕之后,将fwd赋值进入。【以上操作都是采用cms,保证了赋值时的唯一性】
下面是红黑树的扩容:
将将原来的红黑树节点全部替换为新的红黑树节点,并且设置指针。
不同点在于,设置了两个计数器hc与lc,只要转移过去的某一个计数器小于阈值8,将红黑树解除树化,变成普通Node链表。
7.helpTtansfer() 协助扩容方法
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
若传递进来的节点是ForwardingNode,通过他的next指针得到新的Node[] newTable
取得rs值,用于和sc做计算,得到当前扩容的线程数。【其实调用之前已经判别了,这里只是为了防止中间发生了扩容完毕,严谨一点】
while()条件的意思是当前还在扩容中,因为扩容结束后nextTable,table,sc这些属性都会重新赋值,就不为当前线程操作的这些值了
下面的四个条件都是扩容结束的条件,涉及到rs变量和sc变量的运算,还没怎么搞明白,以后再看。
扩容操作就是单纯调用transfer()的逻辑即可,扩容结束,返回新链表。【若未进入if,说明扩容完毕,老数组就是新数组】。
三、1.7与1.8的区别:
HashMap:
注意:转红黑树的条件是:
链表节点数目大于8个,且数组容量达到64位。
ConcurrentHashMap:
最大的区别在于1.7的扩容是单线程扩容,1.8采用多线程扩容。
1.8多了红黑树。1.7采用Segment(继承ReentrantLock可重用锁)多段锁机制;1.8采用TreeBin桶结构。