这个问题可以当做是并发集合这一类问题的代表,所以其他类似的集合也一并提一下:
线程安全集合类可以分为三大类:
- 遗留的线程安全集合如 Hashtable , Vector
- 使用 Collections 装饰的线程安全集合,如:
- Collections.synchronizedCollection
- Collections.synchronizedList
- Collections.synchronizedMap
- Collections.synchronizedSet
- Collections.synchronizedNavigableMap
- Collections.synchronizedNavigableSet
- Collections.synchronizedSortedMap
- Collections.synchronizedSortedSet
- java.util.concurrent.*
java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:
Blocking、CopyOnWrite、Concurrent
- Blocking 大部分实现基于锁,并提供用来阻塞的方法
- CopyOnWrite 之类容器修改开销相对较重
- Concurrent 类型的容器
- 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
- 弱一致性遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
- 求大小弱一致性,size 操作未必是 100% 准确
- 读取弱一致性
我们都知道Hashtable是绝对线程安全的,同样绝对线程安全的Map还有Collections.synchronizedMap,只不过Collections.synchronizedMap是一个装饰器的存在,什么样的Map都能被包装成线程安全的Map,那为什么还要Hashtable呢?因为hashtable是从JDK1.0就开始存在的,而HashMap和Collections.synchronizedMap是从JDK1.2开始的。虽然没什么人用vector,但是Java一直保留Vector和Hashtable,就是因为Vector 是从JDK 1.0开始的,JDK要向下兼容,而且需要避免有些老旧项目出问题。
ConcurrentHashMap,从名字上看就是并发场景用的HashMap,它突出强调的是高并发,并不是什么线程安全,数据一致性和高并发的高吞吐率是矛盾的,ConcurrentHashMap强调的高并发的高吞吐,但是既然是高并发,肯定还是要注意并发的线程安全问题,它几乎将一致性和高并发做到了最佳平衡:
- 只不过将线程安全性的范围缩减至最小(临界代码的范围),才能将高并发的高性能尽可能的发挥到最大。
- 同时,将临界代码缩减到最小,也使得我们的做高并发场景的数据一致性处理的灵活性,展现到最大。
因为我们的业务场景是多变的,可以说有千千万万,线程安全的范围大小的要求肯定也是不尽相同,倘若我某个业务场景,要求数据的强一致性,但是这个需求也不能只有put方法做互斥处理,那就是需要我们自己在对ConcurrentHashMap读写,get和put,做整体线程安全处理(为什么不用hashMap做原子性处理?因为可能同一个业务中的另一个方法共用此Map,但是只需要put方法做互斥,防止扩容出现线程不安全等,后面会重点复现该场景)。那如果不管业务的具体需求,直接就用一个绝对线程安全的Hashtable,想想会发生什么,明明共用该集合的另一个方法只需要做弱数据一致性操作,或者直接就只需要做get操作,却被Hashtable的get方法的互斥处理,导致这业务中两个方法不能并行执行,因为互斥被阻塞,那么肯定不符合高吞吐率的设计要求。就如同我只要鸡翅搭配点薯条就够了,你偏要绑定销售,只肯卖全家桶给我。编程也如同搭积木,肯定是零件的粒度越小,搭造出的模型才能更具有多样性,搭建的工作也更具备灵活性,否则想要的那种模型构造建造做不出,搭出的模型不都一个样吗?
所以很多人一提到ConcurrentHashMap就说它是线程安全的Map集合,虽然也没毛病,因为大部分的先获取再修改的逻辑,也有线程安全的computeIfAbsent,putIfAbsent来实现,但其实还是不太了解它的设计目的,因为它的线程安全范围真的太小了,想要依靠JDK自带的一个集合就可以解决实际应用中复杂多变并发的线程安全和高吞吐量的需求,是很不现实的。捋清楚它的设计目的和思想,那对它的源码解析肯定就好理解一些了。
OK那开始源码解读吧。
static final int spread(int h) {
// 无符号右移16位,因为后面获取hash表index的时候,前期hash表的长度-1后,二进制非0位都在低位,那么key.hashCode()的值的高位就没有用了,
// 高位的hashcode,和低位hashcode明明差距很大,但是因为低位相同,使得它们非常容易碰撞,这里移位就是让hashcode的高16位和低16位都参与到计算来,增加差异化
// 充分散列hash值,最大可能避免碰撞。
// key.hashCode(),一次散列,spread二次散列,然后扩容机制是扩容2倍,使得length() -1 的二进制始终是1111...,再与hash & 碰撞的可能就更新,所以是做了3次散列均匀分布处理
return (h ^ (h >>> 16)) & HASH_BITS;
}
//table初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//将table赋值给tab,同时循环判断table是否为空,不为空说明已经有线程将table初始化了,则直接返回。
while ((tab = table) == null || tab.length == 0) {
//将sizeCtl(sizeControl)赋给sc,并判断是否<0,
if ((sc = sizeCtl) < 0)
//sc < 0说明有线程正在执行初始化操作,此时让出一下cpu,再次被调度后,继续执行while的判断。相当于等待的过程。
Thread.yield(); // lost initialization race; just spin
//如果sc>=0,则说明需要初始化,使用Cas的方式将sizeCtl赋值为-1,这样其他线程进来时就会走到上面的if中去。
//根据返回值判断是否赋值成功,不成功的话,直接进行下一次循环,不成功的情况说明可能其他线程已经在初始化了。
//Unsafe.compareAndSwapInt解释:
//public final native bolean compareAndSwapInt(Object o, long offset, int expected, int x);
//读取传入对象o在内存中偏移量为offset位置的值与期望值expected作比较,相等就把x值赋值给offset位置的值。方法返回true。不相等,就取消赋值,方法返回false。
//具体到下面的if判断就是:
// 检查ConcurrentHashMap对象在内存中偏移量为SIZECTL位置的int值(即为sizeCtl)与sc进行比较,相同就赋值为-1并返回true,不相等则取消赋值并返回false。
// SIZECTL是一个static final的常量,代表在当前ConcurrentHashMap对象中,sizeCtl变量在内存中的偏移量,private static final long SIZECTL;
// 详见ConcurrentHashMap代码最后的static代码块
// U = sun.misc.Unsafe.getUnsafe();
// Class<?> k = ConcurrentHashMap.class;
// SIZECTL = U.objectFieldOffset(k.getDeclaredField("sizeCtl"));
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//赋值成功后进入
//此处体现sizeCtl的一个含义,即sizeCtl = -1,说明正在有线程对table进行初始化。
try {
//再次赋值并判断tab是否为空,双重检查
//防止当前线程在执行上面的if和else if判断期间,有其他线程已经完成Tab的初始化
if ((tab = table) == null || tab.length == 0) {
//如果走到这,说明没有其他线程在对tab进行初始化,且在当前线程初始化完毕之前,不会有其他线程进来(通过sc < 0、U.compareAndSwapInt(this, SIZECTL, sc, -1)和双重检查实现)
//此时sc可能>0,也可能=0,大于0则n赋值为sc,等于0则n赋值为table默认初始大小DEFAULT_CAPACITY=16。
//sc > 0的情况,是调用构造方法时传入了tab的大小。
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//创建一个大小为n的Node<K,V>数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
//新数组赋值给tab和table
table = tab = nt;
// sc 赋值为n*0.75。
// n>>>2 n无符号右移2位为原来的1/4(0.25)
// n减掉n的1/4则为n*0.75,0.75为扩容因子。
sc = n - (n >>> 2);
}
} finally {
//sc赋值给sizeCtl
//此处天sizeCtl的一个含义,即数组扩容的阈值。
sizeCtl = sc;
}
//不管当前线程有没有将table初始化,走到这里说明table已经被初始化完成了,可以跳出循环了
break;
}
}
return tab;
}
//帮助扩容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
//判断oldtable(即为传入的tab) 不为空 , 传入的当前f节点是ForwardingNode
//而且nextTab = f.nextTable 不为空,为空说明尚没有线程对老表进行扩容,扩容迁移期间,老表已经迁移完的节点会置为ForwardingNode,
//ForwardingNode的nextTable属性为迁移后的新表,这是为了在迁移过程中,如果有其他线程访问老表已经迁移完的元素查数据,可以通过nextTable查询。
//满足以上条件,额进入if开始扩容
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
//仍然是先根据老表的长度计算扩容标识戳
int rs = resizeStamp(tab.length);
//nextTab == nextTable && table == tab 确认新还是扩容的新表,老表还是被扩容的老表。
//(sc = sizeCtl) < 0 判断当前是否仍在扩容过程中 sizeCtl = -1怎么办?
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// (sc >>> RESIZE_STAMP_SHIFT) != rs 判断是否是当前表的扩容标识戳,建addCount方法
// sc == rs + 1 || sc == rs + MAX_RESIZERS 判断是否已经所有线程退出扩容或达到扩容线程数上线
// 同addCount方法一样,这个地方同样有存在bug,
// 应该是应该是sc == (rs << RESIZE_STAMP_SHIFT) + 1 || sc == (rs << RESIZE_STAMP_SHIFT) + MAX_RESIZERS
// transferIndex <= 0 transferIndex的含义是,当前最小的已被分配给线程的数组元素下标,因为数组是从高到底迁移的,因此这个变量可以代表多线程迁移数组的进度。
// 小于等于0表示,当前数组的所有元素都已经分配给线程处理,当前线程不需要再帮助扩容了。
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
//使用cas的方式将sizeCtl + 1,可以理解为将低16位+1,扩容过程中的sizeCtl具体值没有意义,
//高16位为扩容标识戳,低16位为扩容线程数+1,应该这样去理解。
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
// 调用扩容方法
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
//根据数组长度n计算该数组扩容的标识戳
static final int resizeStamp(int n) {
//Integer.numberOfLeadingZeros(n) 该方法用来返回n的二进制数最高非0位之前0的个数,因为n是2的次幂,因此低位都是连续的1,高位都是连续的0,不通的高位0的个数是不一致的。
//(1 << (RESIZE_STAMP_BITS - 1) ,RESIZE_STAMP_BITS=16,因此这一句的含义是1右移15位,即为0000 0000 0000 0000 1000 0000 0000 0000
//例n = 16 则n的2进制为 0000 0000 0000 0000 0000 0000 0001 0000
//Integer.numberOfLeadingZeros(n) = 27, 转换为2进制为 0000 0000 0000 0000 0000 0000 0001 1011
//或的结果
// 0000 0000 0000 0000 0000 0000 0001 1011
// 或0000 0000 0000 0000 1000 0000 0000 0000
//等于0000 0000 0000 0000 1000 0000 0001 1011
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
//线程扩容方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//计算数据迁移的步长,即每个线程应该迁移多少个元素
//NCPU>1标识多核CPU,多核cpu的情况下 将table.length 除以 8(即n >>> 3)后再除以cpu树,单核cpu不做处理。暂时不知道为啥这样计算
//如果计算后的步长小于MIN_TRANSFER_STRIDE(值为16),则步长改为16
//即每个线程至少迁移16个元素
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")
//创建一个大小为2n的Node数组。
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
//赋值给nextTab
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
//内存溢出时,帮助退出外层循环
sizeCtl = Integer.MAX_VALUE;
return;
}
//赋值给nextTable
nextTable = nextTab;
//初始化transferIndex 为数组长度 n
//private transient volatile int transferIndex;
//用来记录多线程进行数据迁移时,当前最小的已被分配给线程的数组元素下标
//因为concurrentHashMap是按照数组下标从大到小迁移,因此初始值为tab的长度n
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
// 外层循环用来控制循环迁移每一个需要当前线程迁移的元素
// concurrentHashMap数据迁移是按照数组下标从大到小迁移,首先给当前线程分配一个步长范围,然后在步长范围内从大到小挨个迁移。
// 当前步长迁移完毕后,继续计算下一个需要该线程迁移的步长,多线程帮助扩容的话,不通步长的范围边界是不连续的。
for (int i = 0, bound = 0;;) {
//f:当前要迁移的节点元素节点,fh:当前要迁移元素节点的额hash值
Node<K,V> f; int fh;
//这个while循环用来计算每次要处理的数组元素索引i和当前步长处理完后寻找新的步长范围。
//advance = true,代表需要继续计算元素索引i或新的步长范围。
while (advance) {
//nextIndex:下一个要处理的步长范围上限+1,nextBound:下一个要处理的步长范围下限
int nextIndex, nextBound;
//这个if用来判断数组元素索引i是否到了当前步长范围的下限边界,如果还不到,则跳出while循环,继续处理--i位置的元素
//如果finishing=true则跳出while循环,不再计算步长和索引
if (--i >= bound || finishing)
//跳出循环
advance = false;
//检查tab的所有元素是否都已分配完毕,并将transferIndex赋值给nextIndex
else if ((nextIndex = transferIndex) <= 0) {
//这个赋值是为了循环跳出后,当前线程能够走进下面的第一个if判断,去退出扩容
i = -1;
//跳出循环
advance = false;
}
//走到这说明当前线程已经将当前步长处理完毕(不满足--i>=bound)条件
//且数组元素尚未迁移完毕(不满足finishing = true),且数组的元素上没有完全分配给线程处理(不满足transferIndex <= 0)
//此时使用cas的方式将TRANSFERINDEX修改为下一个可处理步长范围的下线,
//代表当前线程已经分配到该位置了,下一个线程来请求分配步长的时候,应该从此处开始分配。
//同时完成给nextBound赋值
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//本次步长范围的下限
bound = nextBound;
//本次步长范围开始处理的第一个元素下标(最大的)
i = nextIndex - 1;
//跳出循环
advance = false;
}
}
// i<0 确保while循环中 第二个if判断[else if ((nextIndex = transferIndex) <= 0)]后可以走进来
// i >= n,i + n >= nextn 这两个条件是等价的,确保不会超过两个数组的最大值(没找到什么时候会触发这个条件)
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//finishing == true 说明已经迁移完毕
// finishing = true是在下面的if中赋值的,走进这个循环已经是迁移完毕后再次检查也结束了。
if (finishing) {
//完成table的赋值
nextTable = null;
table = nextTab;
//重新给sizeCtl赋值为扩容阈值,这个写法的结果是2n*0.75,应该都能看懂了吧
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//线程走到这说明当前线程已经完成了迁移,依次使用cas的方式将sizeCtl-1后退出。
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT 这个条件是为了判断是否是最后一个线程
//最后一个线程迁移完毕后,不能拍拍屁股就走了,还有其他工作要做。
//记不记得首次有线程扩容时sc=(rs << RESIZE_STAMP_SHIFT) + 2,所以当(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT时,说明还不是最后一个线程
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
//判断还不是最后一个线程,则允许退出
return;
//最后一个线程会走到这里,继续其他工作
//advance = true 是需要走到上面的while循环中,继续--i
//finishing = true 是说明迁移工作已经完成了
finishing = advance = true;
//i重新赋值为n,走上面while循环中的--i,再把所有的元素都检查一遍。
i = n; // recheck before commit
}
}
// 没有走进上面的if,说明当前步长的迁移工作尚未完成
// 此处判断一下当前元素是否为null
else if ((f = tabAt(tab, i)) == null)
//如果为null则使用cas的方式直接把改元素赋值为forwardingNode,代表此处已迁移完毕。
//修改成功过后,使用advance控制进行while循环
//说一下cas失败的情况:cas失败可能是有其它线程正在对当前元素进行赋值,此时advance=false,重新否for循环后不会进入到while循环
// i的值不会变,下次循环进来还是处理这个元素,如果此时赋值完成了,则会走到下面的else中去完成迁移。
advance = casTabAt(tab, i, null, fwd);
//根据当前元素的的哈希值判断是否是已经迁移完的元素,即forwardingNode,迁移完毕后的检查会走到这里。
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
//走到这个else说明当前节点(下标为i)需要进行迁移
//上来还是先锁定当先数组元素对象(对于链表来说是头结点,对于红黑树来说是treebin节点)。
synchronized (f) {
//再次检查当前节点是否还是f节点,如果不是则啥也不干直接退出
//什么情况下会不相等呢,可能是有其他线程正在remove该节点吧
if (tabAt(tab, i) == f) {
//还是f,开始迁移
Node<K,V> ln, hn;
//fh >=0 说明是链表,其实应该不会有0的情况
if (fh >= 0) {
//注意此处跟HashMap类似,不会对节点再次计算哈希值,而是利用table的长度为2的次幂的原理,通过位运算来获取新的位置。
//n是2的次幂,因此2进制下,只有一位是1,假设n=16,则2进制为 0000 0000 0000 0000 0000 0000 0001 0000
//因此fh & n的结果只有 0 和 n 两个值。
//而根据fh计算数组下标的逻辑为 fh&(n-1)(n为2的次幂的情况下等价于对求余,可以自己算一算)
// n=16的情况下 n-1 的2进制为 0000 0000 0000 0000 0000 0000 0000 1111
// 2n-1 的2进制为 0000 0000 0000 0000 0000 0000 0001 1111
//因此 fh & (n-1)和 fh & (2n-1)的区别,仅仅是第5位是0或1的区别(n=16的情况下),反映到10进制就是 i 和 n + i的区别。
//可以得出一个结论:这一位的值为0,说明该节点还是这个下标,称之为低位;为1,说明该节点新的下标为n+i,称之为高位。
//runBit就是计算出来的这一位的值(十进制),用来区别高位和低位。
int runBit = fh & n;
//lastRun,这点跟HashMap不同,HashMap是从链表的头一个一个迁移
// 而ConcurrentHashMap会先计算该链表最后几个runBit位相同的Node,这几个不用一个一个迁移,可以作为一个子链表,直接挂过来。
// lastRun就是用来记录这个子链表的头结点。
//初始值为f,从链表头开始遍历。最好的情况下,整条链表的runBit是一样的,直接挂上即可;最坏的情况,最后两个节点的runBit不一致,只有最后一个节点能直接挂过来。
Node<K,V> lastRun = f;
//遍历链表开始循环
for (Node<K,V> p = f.next; p != null; p = p.next) {
//计算当前节点的runBit值
int b = p.hash & n;
//如果runBit有变化,则记录一下
//循环结束后,lastRun即为子链表的头结点
// runBit即为子链表的高位和低位标识
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
//runBit == 0 说明子链表是低位,先把子链表挂到低位ln,此时高位hn上没有节点,因此为null
if (runBit == 0) {
ln = lastRun;
hn = null;
}
//else(runBit == n),先把子链表挂到高位hn,此时低位ln上没有节点,因此为null
else {
hn = lastRun;
ln = null;
}
//开始遍历链表上剩余的元素,从头结点f开始,到子链表的头结点lastRun的前一个节点为止。
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
//这就很好理解了,低位则根据当前节点的值声明一个节点挂到ln上,注意是每次插入都是从链表头插入。
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
//高位则根据当前节点的值声明一个节点挂到hn上,注意是每次插入都是从链表头插入。
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//将低位链表挂到nextTab的i位置。
setTabAt(nextTab, i, ln);
//将高位链表挂到nextTab的 i+n 位置。
setTabAt(nextTab, i + n, hn);
//将oldTable的i位置置为forwardingNode,代表该处已经迁移完毕。
setTabAt(tab, i, fwd);
//advance置为ture,继续走到上面的while循环中。
advance = true;
}
//判断一下f是不是红黑树节点,TreeBin的hash为-2,因此不会走到上面的链表处理中
else if (f instanceof TreeBin) {
//红黑树的迁移也比较简单,因为在链表转红黑树时,并没有破坏原有的链表结构,数据的增删改,也维持了链表结构。
//所以,根据链表即可以遍历所有的元素。
//这里多说一点,红黑树的节点TreeNode继承自链表的Node节点,保留了Node的next属性,但TreeNode还有自己的pre属性,这个是Node没有的,为什么要增加一个pre改成双向链表呢。
// 原因是:红黑树的删除操作要维持链表结构的话,就需要在remove掉一个节点后,把节点在链表上的前后位置的节点连接起来
// 而红黑树的remove操作是根据红黑树查找的算法找到的这个节点,如果只维持一个next属性的话,删除时,并不知道当前节点的前一个节点是谁,没法连接起来。
// 链表不存在该问题,因为链表从头开始遍历,肯定是从前一个节点遍历过去的。
//言归正传,先把f节点转换为TreeBin节点,并赋值给t
TreeBin<K,V> t = (TreeBin<K,V>)f;
//声明两个变量,记录一下低位链表的头和尾
TreeNode<K,V> lo = null, loTail = null;
//声明两个变量,记录一下高位链表的头和尾
TreeNode<K,V> hi = null, hiTail = null;
//低位和高位Node计数
int lc = 0, hc = 0;
//使用链表结果循环遍历
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
//将当前节点的值创建为TreeNode
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
//计算高位还是低位
if ((h & n) == 0) {
//loTail为null标识是首次向链表插入数据
//同时将当前节点的前一个节点指向尾结点,实现双向链表
if ((p.prev = loTail) == null)
//首次插入,将节点赋值给链表头
lo = p;
else
//否则将节点挂在链表尾,跟上面不同,这里是从尾部插入
loTail.next = p;
//这是后p是链表尾了,将p赋值给链表尾
loTail = p;
//计数
++lc;
}
else {
//同低位一样的操作,不再赘述
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
//有点复杂,注意看
//(lc <= UNTREEIFY_THRESHOLD) 如果低位的节点数小于树化的临界值,则将低位链表lo反树化(untreeify(lo))
// 否则再看高位计数(hc)是不是0,如果高位计数是0,说明都是在低位链表上,这样直接把原来的TreeBin节点t拿过来,不用在耗费资源进行树化了。
// 如果高位计数不是0,说明既有高位又有低位,则将lo链表初始化为TreeBin(new TreeBin<K,V>(lo)),初始化TreeBin的过程中会将链表树化。
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;
//将低位ln(可能为链表或树)挂在nextTab的i位置。
setTabAt(nextTab, i, ln);
//将高位ln(可能为链表或树)挂在nextTab的i + n位置。
setTabAt(nextTab, i + n, hn);
//将oldTable的i位置置为forwardingNode,代表该处已经迁移完毕。
setTabAt(tab, i, fwd);
//advance置为ture,继续走到上面的while循环中。
advance = true;
}
}
}
}
}
}
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 其中 spread 方法会综合高位低位, 具有更好的 hash 性
int hash = spread(key.hashCode());
int binCount = 0;
// 开始自旋
for (Node<K,V>[] tab = table;;) {
// f 是链表头节点
// fh 是链表头结点的 hash
// i 是链表在 table 中的下标
Node<K,V> f; int n, i, fh;
// 要创建 table
if (tab == null || (n = tab.length) == 0)
// 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环
tab = initTable();
// 要创建链表头节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 添加链表头使用了 cas, 无需 synchronized
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) {
// 再次确认链表头节点没有被移动
if (tabAt(tab, i) == f) {
// 链表
if (fh >= 0) {
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 找到相同的 key
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;
// 已经是最后的节点了,新增 Node, 追加至链表尾
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;
// putTreeVal 会看 key 是否已经在树中, 是, 则返回对应的 TreeNode
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)
// 如果链表长度 >= 树化阈值(8), 进行链表转为红黑树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
增加 size 计数
addCount(1L, binCount);
return null;
}
// check 是之前 binCount 的个数
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 已经有了 counterCells, 向 cell 累加
if ((as = counterCells) != null ||
// 还没有, 向 baseCount 累加
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (
// 还没有 counterCells
as == null || (m = as.length - 1) < 0 ||
// 还没有cell
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
// cell cas 增加计数失败
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 创建累加单元数组和cell, 累加重试
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;
// newtable 已经创建了,帮忙扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 需要扩容,这时 newtable 未创建
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//如果哈希表长度小于64,那么选择扩大哈希表的大小,而不是把链表转为红黑树
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
//将哈希表中index位置的链表转为红黑树
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
//下面逻辑将node链表转化为TreeNode链表
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代表红黑树,将TreeBin保存在哈希表的index位置
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
//size为32
//sizeCtl为原大小16的3/4,也就是12
private final void tryPresize(int size) {
//根据tableSizeFor计算出满足要求的哈希表大小,对齐为2的n次方。c被赋值为64,这是扩容的上限,扩容一般都是扩容为原来的2倍,这里c值为了处理一些特殊的情况,确保扩容能够正常退出。
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
//此时sc和sizeCtl均为12,进入while循环
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
//这里处理的table还未初始化的逻辑,这是由于putAll操作不调用initTable,而是直接调用tryPresize
if (tab == null || (n = tab.length) == 0) {
//putAll第一次调用时,假设putAll进来的map只有一个元素,那么size传入1,计算出c为2.而sc和sizeCtl都为0,因此n=2
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
//经过计算sc=2
sc = n - (n >>> 2);
}
} finally {
//sizeCtl设置为2.第二次循环时,因为sc和c相等,都为2,进入下面的else if分支,结束while循环。
sizeCtl = sc;
}
}
}
//扩容已经达到C值,结束扩容
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
//table已经存在,那么就对已有table进行扩容
else if (tab == table) {
int rs = resizeStamp(n);
//sc小于0,说明别的线程正在扩容,本线程协助扩容
if (sc < 0) {
Node<K,V>[] nt;
//判断是否扩容的线程达到上限,如果达到上限,退出
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//未达上限,参与扩容,更新sizeCtl值。transfer方法负责把当前哈希表数据移入新的哈希表。
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//本线程为第一个扩容线程,transfer第二个参数传入null,代表需要新建扩容后的哈希表
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, 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());
//这个if判断中做了如下几件事情:
//1、哈希表是否存在
//2、哈希表是否保存了数据,同时取得哈希表length
//3、哈希表中hash值映射位置保存的对象不为null,并取出给e,e为链表头节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//如果e的hash值和传入key的hash值相等
if ((eh = e.hash) == h) {
//如果e的key和传入的key引用相同,或者key eaquals ek。那么返回e的value。
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//如果头节点的hash<0,有两种情况
//1、hash=-1,正在扩容,该节点为ForwardingNode,通过find方法在nextTable中查找
//2、hash=-2,该节点为TreeBin,链表已经转为了红黑树。同样通过TreeBin的find方法查找。
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//以上两种条件不满足,说明hash映射位置保存的还是链表头节点,但是和传入key值不同。那么遍历链表查找即可。
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (key == null || mappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());
V val = null;
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
// 先创建一个占位空节点
// 锁①处
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
// 如果cas成功,那么其他线程在锁②处会和这里锁①的逻辑互斥,保证了在初始化表头的逻辑线程安全
// 那为什么不将这里的synchronized (r)放到cas的后面,如果放到cas后面,cas成功了,但是这里却抢占不到锁怎么办?
// 被锁②先抢到锁之后,已经设置好表头next的链表结构会被丢失到Map的作用范围之外,进而会导致内存泄漏,这是非常严重的后果。
// 如果其他的线程也在锁①处先获取锁,那肯定是获取成功的,因为每个线程执行到这里获取的都是获取new ReservationNode<K,V>()的锁,
// 但是却只有一个线程cas成功,cas失败的线程,这里获取的无用的锁,自然释放,占位空节点对象也会被GC回收,幸好空节点对象占用的内存不大,所以GC也不会有太大的压力
// cas失败很可能就是到锁②处获取表头的锁,再不就是帮忙扩容了。
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
if ((val = mappingFunction.apply(key)) != null)
node = new Node<K,V>(h, key, val, null);
} finally {
setTabAt(tab, i, node);
}
}
}
if (binCount != 0)
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
boolean added = false;
// 锁②处
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek; V ev;
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
val = e.val;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
if ((val = mappingFunction.apply(key)) != null) {
added = true;
pred.next = new Node<K,V>(h, key, val, null);
}
break;
}
}
}
else if (f instanceof TreeBin) {
binCount = 2;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(h, key, null)) != null)
val = p.val;
else if ((val = mappingFunction.apply(key)) != null) {
added = true;
t.putTreeVal(h, key, val);
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (!added)
return val;
break;
}
}
}
if (val != null)
addCount(1L, binCount);
return val;
}
下面复现我上面说的同一业务中两个方法共用一个数据集合(通常是本地缓存),一个强一致性,一个弱一致性。
假设业务场景中需要对一类型的产品的某些数据进行本地缓存,有一个方法A要对一些产品批次号的使用次数进行累加并缓存,如果使用次数超过了一个阈值,就要产生一个新的批次号,并且从0开始缓存该批次号的使用次数。这是用computeIfAbsent()、putIfAbsent()等方法不太容易能一步到位解决的,还是有读写交错的存在,需要对这段代码做线程安全处理。
//伪代码:
String word = null;
while(word == null){
word = map.keySet().foreach(key, {
if(key.startWith(productName)){
return key;
}
})
if(word == null){
Thread.sleep(10);
}
}
synchronized(word){
Long useNum = map.get(word).increment().longValue();
if (useNum > MAXVALUE){
String newWord = getNewWordA();
map.remove(word);
map.put(newWord, new LongAdder());
}
// 新的批次号更新到数据库,并将原来的批次号的数据持久化到数据库,并将该批次号置为已弃用
......
}
另外一个方法B是对另外一类产品批次号进行缓存,需要进行使用次数缓存,但对使用次数没有限制,和方法A的批次号的位数都不一样,产生新的批次号的逻辑也不一样,也没有并发产生新的批次号:定时任务每过一段时间,产生一个新的批次号。
String word = null;
while(word == null){
word = map.keySet().foreach(key, {
if(key.startWith(productName)){
return key;
}
})
if(word == null){
Thread.sleep(10);
}
}
synchronized(word){
// 更新该批次号的使用次数到数据库
Long useNum = map.get(word).increment().longValue();
}
// 定时任务
map.remove(word);
map.put(newWord, new LongAdder());
synchronized只对处理同一个批次号的并发线程才有互斥阻塞的作用,也就是说,即使加了锁,方法A本身还会有并行执行的时候,如果不采用一个对put()方法做线程安全处理的集合,还是会有线程安全的问题,所以单纯使用hashMap外面加synchronized、AQS锁(上述代码中的map用HashMap实现),首先就被淘汰了。可是对于ConcurrentHashMap,就算不同的批次号word的hash发生了重合,或者(length - 1) & hash散列hash表位置的时候发生碰撞,也有ConcurrentHashMap put方法对每个桶的表头加锁,来保证扩容和添加的并发线程安全。
如果我们使用Hashtable 来做的话,方法A本身并发,方法A和方法B并发执行的批次号就算不一致,也会被互斥阻塞,就算我们用HashMap,带上读写锁 ReentrantReadWriteLock来扩展一个线程安全集合(MyConcurrentHashMap extends HashMap)实现,代入到A方法的代码中,其并发的吞吐量肯定远不及ConcurrentHashMap的。
所以如果遇到并发下,读写交错的操作,需要满足一定的数据一致性场景,就干脆用Hashtable,我个人拙见是非常不可取的,不说对比读写锁配合HashMap的并发吞吐率,就是A方法在执行,B方法的get()这一步都没有办法并行执行。更不要说方法A本身多线程在不同的批次号的情景下的并行执行了。
主要是JDK自带的绝对线程安全类,hashtable,Collections.synchroniedMap只会无脑每个方法加synchronized就行了,真的一点都不比我们自己写的加锁代码优秀。如果遇到ConcurrentHashMap原生API方法也不能解决的并发线程安全问题,就直接用hashtable,Collections.synchroniedMap 那么JDK的作者,费心费力的写个ConcurrentHashMap,岂不是白费心力?
更多资源分享,请关注我的公众号:搜索或扫码 砥砺code