JDK1.7和JDK1.8区别:
在1.7版本时候,ConcurrentHashMap是由一个个Segment组成,通过继承ReentrantLock来进行加锁,通过每次锁住一个segment来保证每个segment内的操作的线程安全性从而实现全局线程安全
相比于1.7版本,1.8进行了两个改进:
1:取消了Segment分段设计,直接使用Node数组来保存数据,并且采用Node数组元素作为锁来实现每一行数据进行加锁来进一步减少并发冲突概率
2:原本是数组+单向链表的数据结构变成了数组+链表+红黑树的结构
源码分析:
第一部分: final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode());// hash值 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) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin }
initTable(); 初始化数组
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) {//当tab未被初始化时候进入 if ((sc = sizeCtl) < 0)//当有多个线程竞争时候直接让出CPU Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//通过cas操作,将sizeCtl替换成-1表示当前线程有初始化资格 try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//默认的初始化容量16 @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2);//下次扩容大小12 } } finally { sizeCtl = sc; } break; } } return tab; }
tabAt 该方法获取对象中的offset偏移地址来获取field值,实际上和tab[i]等价,table使用volatile修饰,写操作happen-before与volatile读操作,因此其他线程对table的修改均对get读取可见,虽然table数组添加了volatile关键字,但是volatile关键字只针对数组引用具有volatile的语义,而不是它的元素
总结:第一部分仅仅只是初始化数组和通过偏移地址获取table对应的位置,然后添加数据
第二部分:
在添加完数据后,会通过addCount()来增加ConcurrentHashMap中的元素个数
整个过程可以理解为使用一个baseCount表示没有线程竞争时候的计数,使用CounterCell[]数组来存储不同线程统计的值,这样
比使用单个baseCount对象进行加锁效率更高
addCount(1L, binCount); binCount:链表长度
private final void addCount(long x, int check) { CounterCell[] as; long b, s; //如果CounterCell[]数据为空,则通过cas修改baseCount变量,对这个变量进行原子累加操作(在没有竞争情况下,采用baseCount来记录元素个数) //如果cas失败表示存在竞争,这个时候不能用baseCount来累加,而是通过CounterCell来记录 if ((as = counterCells) != null ||!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; //CounterCell为空或者计数表中随机取出一个数组的位置为口为空或者cas修改CounterCell随机位置的值,如果失败表示 //出现并发情况,这些情况下直接调用fullAndCOunt 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(); } ...... } }
private final void fullAddCount(long x, boolean wasUncontended) { int h; //获取当前线程的probe值,如果值为0,则初始化当前线程的probe的值 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; //如果CounterCell已经被初始化过了 if ((as = counterCells) != null && (n = as.length) > 0) { if ((a = as[(n - 1) & h]) == null) { // CounterCell不在初始化或者扩容状态下 if (cellsBusy == 0) { CounterCell r = new CounterCell(x); // Optimistic create //CAS设置cellsBusy标识,防止其他线程来对counterCells并发处理 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; } //在addCount方法中cas失败了,并且获取probe的值不为空,设置为未冲突标识,进入下一次自旋 else if (!wasUncontended) // CAS already known to fail wasUncontended = true; // Continue after rehash //由于指定下标位置的cell值不为空,则直接通过cas进行原子累加,如果成功则直接退出 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; //这里表示CounterCell数组容量不够,线程竞争较大,所以先设置一个标识表示正在扩容 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); } //如果cellsBusy标志位为0,CounterCell未初始化过,cas修改cellBusy成功 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]; //将x也就是元素的个数放在指定的数组下标位置,此处x=1 rs[h & 1] = new CounterCell(x); counterCells = rs; init = true; } } finally { cellsBusy = 0; } if (init) break; } else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x)) break; // Fall back on using base } }
判断是否需要扩容,也就是当更新后的键值对总数baseCount>=阈值sizeCtl时候进扩容
1:如果当前正在处于扩容阶段,则当前线程会加入并协助扩容
2:如果当前没有在扩容,则直接触发扩容操作
if (check >= 0) { Node<K,V>[] tab, nt; int n, sc; //s表示集合大小,如果集合大小大于或者等于扩容阈值(默认的0.75)并且table不为空,且table的长度小于最大容量 while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { //生成一个和扩容有关的扩容戳 int rs = resizeStamp(n); //sc<0也就是sizeCtl<0,说明已经有别的线程正在扩容了 if (sc < 0) { //下列条件有一个为true说明当前线程不能帮助进行扩容 //1: (sc >>> RESIZE_STAMP_SHIFT) != rs 表示较高RESIZE_STAMP_BITS位生成戳和rs不相等 //sc=rs+1表示扩容结束 //sc==rs+MAX_RESIZERS 表示帮助线程已经达到最大值了 //nt=nextTable--->扩容已经结束 //transferIndex<=0 表示所有的transfer任务都被领取完了,没有剩下的hash桶进行transfer 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)) transfer(tab, nt); } else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)){ transfer(tab, null); } s = sumCount(); } }
扩容transfer:
ConcurrentHashMap并没有直接加锁,而是采用CAS实现无锁的并发同步策略,还可以利用多线程来进行协同扩容
它将Node数组当做多个线程之间共享的任务队列,然后通过维护一个指针来划分每个线程锁负责的区间,每个线程通过区间逆向遍历来实现扩容,一个已经迁移完的bucket会被替换为一个ForwardingNode节点,标记当前bucket已经被其他线程迁移完了
假设table数组总长度是64,默认情况下每个线程可以分到16个bucket,然后每个线程处理自己的方位,按照倒序来做迁移
1:ForwardingNode(fwd)这个类是一个标识类,用于指向新表,其他线程遇到这个类会主动跳过这个类,因为这个类要么扩容迁移正在进行,要么就是已经完成扩容迁移,也就是这个类要保证线程安全再进行操作
2:advance:该变量用于提示代码是否进行推进处理,也就是当前桶处理完,处理下一个桶的标识
3:finishing:用于提示扩容是否结束用的
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 //nextTab未初始化,nextTab是用来扩容的node数组 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; //创建一个fwd节点,表示一个正在被迁移的Node并且它的hash值为-1(MOVED),在原数组中位置i处的节点完成迁移后,就会 //在i位置设置一个fwd来告诉其他线程这个位置已经处理过了 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); //首次推进为true,说明再次推进一个下标(i--),反之如果是false,那么就不推进下标,需要将当前的下标处理完毕才能继续推进 boolean advance = true; //判断是否已经扩容完成,完成就return,退出循环 boolean finishing = false; // to ensure sweep before committing nextTab //通过for循环处理每个槽位中的链表元素,通过cas设置transferIndex属性,并初始化i和bound值,i指当前处理的 //槽位序号,bound指需要处理的槽位边界,先处理槽位15的节点 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 ........ } }
ThreadA进行扩容
槽位15中没有节点,则通过CAS插入在第二步中初始化的ForwardingNode节点,用于告诉其他线程该槽位已经处理过了
数据迁移:
//对数组该节点位置加锁,开始处理数组该位置的迁移工作 synchronized (f) { //再次校验 if (tabAt(tab, i) == f) { //ln表示低位,hn表示高位 Node<K,V> ln, hn; //将链表拆分成两个部分,0在低位,1在高位 if (fh >= 0) { 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; }
//红黑树部分数据迁移
..............
ConcurrentHashMap采用高低位进行扩容,生成ln链和hn链,然后通过CAS操作,将hn链放在i+n也就是14+16的位置,ln链保持原来的位置不动,并且设置当前节点为fwd,表示已经被当前线程迁移完了
采用高低位扩容原因:
(f = tabAt(tab, i = (n - 1) & hash)) == null
通过(n-1)&hash来获取在table中的数组下标来获取节点数据
假设table的长度为16,二级制是0001 0000,减一后二进制是 0000 1111,如果某个key的hash值为9,对应的二进制是0000 1001,那么按照(n-1)&hash的算法结果是9 [00001111 & 00001001]=9,此时扩容后16变成了32,那么n-1二级制是0001 1111
此时用hash值为9计算仍旧为9
如果key的hash值为20,按照计算16位的时候是0000 0100,32位则是0001 0100,位置不同,所以需要迁移,16位到32位正好增加了16,所以对于高位,直接增加扩容的长度,这样就不需要在每次扩容时候来重新计算hash,提升了效率
第三部分:
如果对应的节点存在,判断这个节点是不是等于MOVED(-1),说明当前节点是ForwardingNode节点,意味着有其他节点正在进行扩容,那么就直接帮助他扩容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { Node<K,V>[] nextTab; int sc; //判断此时是否仍然在执行扩容,nextTab=null的时候说明扩容已经结束了 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; //如果CAS成功则进行协助扩容 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { transfer(tab, nextTab); break; } } return nextTab; } return table; }
第四部分:
如果被添加的节点的位置已经存在节点时候,需要以链表的方式加入到节点中,如果当前节点已经是一颗红黑树,那么就会按照红黑树的规则将当前节点加入到红黑树中
//此时说明f是当前nodes数组对应位置节点的头节点,并且不为空 else { V oldVal = null; //对应头节点位置加锁 synchronized (f) { //再次判断 if (tabAt(tab, i) == f) { //头节点的hash值大于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) { //如果链表长度到达临界点8,需要将链表转换成树结构 if (binCount >= TREEIFY_THRESHOLD) //如果链表长度大于8则要触发扩容或者红黑树的转换操作 treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } }
private final void treeifyBin(Node<K,V>[] tab, int index) { Node<K,V> b; int n, sc; if (tab != null) { //table的长度是否小于64,如果是则进行扩容,否则将链表转换成红黑树结构 if ((n = tab.length) < MIN_TREEIFY_CAPACITY) tryPresize(n << 1); else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { synchronized (b) { 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; } setTabAt(tab, index, new TreeBin<K,V>(hd)); } } } } }
扩容的触发有两种情况:
1:当addCount()统计容器大小时候,如果超过阈值则进行扩容
2:当链表长度超过8,并且table的长度大于64时候进行扩容,如果只是链表长度超过8则进行链表转红黑树