ConcurrentHashMap 源码分析 主要涉及多线程扩容和容量增加以及帮助扩容

ConcurrentHashMap 源码分析 主要涉及多线程扩容和容量增加以及帮助扩容

准大四生为了秋招,刚好也方便复习写一写,希望各位大佬或者面试官看到了能指出不对地方

本文依然从最常用的put方法入手,我也看到很多博客的put方法的讲解,我就先简单讲一些大家都讲的东西,然后再讲一些不是每个人都讲的,例如 ** addCount(1L, binCount);*方法和树插入还有helpTransfer等等。
jdk版本1.8 由于没用过1.7也没看过。只是再网上看过一些博客 就不讲1.7了

先来说一下总体操作吧:
1 死循环变量hash桶也就是table,然后第一次插入就初始化table (等下讲初始化方法)。
2 然后如果算出hash桶中的位置如果为空就用cas插入(cas就是的原理应该是利用对总线上锁,某一时刻只能有一个cup对数据修改成功。其他线程再此修改就会发现 预期值和旧值不等,修改失败)
3 然后判断当前hash是否等于MOVED的这个状态表示此时数组正在扩容,我们这个线程要去帮助扩容,也就是说支持多线程扩容(等下讲这个方法)
4 然后就是锁住hash桶的头节点 进入插入操作 该更新就执行更新操作,该插入就插入,如果是树就执行树的插入操作(这里的红黑树是用的TreeBin TreeBin里面封装了 TreeNode为什么这样做了 是因为一棵红黑树的插入操作可能会引起根节点的变化 也就是hash桶的头节点,而我们锁住的正是头节点,一旦头节点一变锁的对象就不是头节点了,而用Treebin来封装了TreeNode 保证每次头节点都是同一个Treebin对象。)
5 然后最后判断前面技术bincount是否大于阈值 ,大于就树化(当然容量必须大于64才树化否则扩容)
6 然后进行增加size操作(这里也对多线程采取了优化, 里面通过一个CounterCell数组和basecount值相加。等下详细讲)

  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;;) {
     //一个for的死循环 
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable(); //第一次插入 hash桶为空 初始化
                
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
     
            //算出当前的hash桶位置没有元素 直接利用cas插入   
            //cas就是的原理应该是利用对总线上锁,某一时刻只能有一个cup对数据修改成功。
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }

            //这个方法等下讲 如果进入这个条件 表示当前map正在扩容 我应该去帮助他扩容。也就是多线程扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);

//前面条件都不满足 开始进入插入操作,对hash桶的头节点上锁 
            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;
                                //这里是插入新值操作
                                if ((e = e.next) == null) {
     
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //这里判断是树  hashmap里面用的树是TreeNode 这里用的是TreeBin TreeBin里面封装了 TreeNode为什么这样做了 是因为一棵红黑树的插入操作可能会引起根节点的变化 也就是hash桶的头节点,而我们锁住的正是头节点,一旦头节点一变锁的对象就不是头节点了,而用Treebin来封装了TreeNode 保证每次头节点都是同一个Treebin对象。
                        else if (f instanceof TreeBin) {
     
                            Node<K,V> p;
                            binCount = 2;
                            //这里的树的插入在我前面讲过的文章hashmap里面又是差不多的 就不在这里继续讲了 有兴趣可以去看一下。
                            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;
                }
            }
        }
        //这里就是增加size的方法 里面通过一个CounterCell数组和basecount值相加。等下详细讲
        addCount(1L, binCount);
        return null;
    }

为了衔接上次讲的hashmap 树化过程 这里先讲 concurrenthashmap的书画过程 其实大同小异

 private final void treeifyBin(Node<K,V>[] tab, int index) {
     
        Node<K,V> b; int n, sc;
        if (tab != null) {
     
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
     
            //树化肯定不能释放头节点的锁,在前面的插入代码完成他释放了锁 所以这里需要重新获取
                synchronized (b) {
     
                    //下面就和前面讲的hashmap一样 如果没看过可以看一下
                    //也就是把单向链表转换成双向链表
                    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;
                        }
                        //利用cas 吧tab的index位置改成了 Treebin数据结构包装的红黑树
                        //前面已经讲过为什么用Treebin而不是TreeNode防止插入时红黑树头节点变化导致所致的头节点变化了
                        //newTreebin过程设计了 红黑树的插入旋转啊变色 这些都是在前面hashmap讲过就不重复了
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

初始化hash桶

盘点hash桶是否为空 如果为空 并且 sizeCtl(默认等于0)不小于0 就利用cas修改为-1
最终把他修改为 0.75*n 如果在初始化过程的过程中来了 线程就调用yiled 让出cup执行权限当然自己也会去参与抢夺cup权限的过程。在前面那个线程初始化成功 新的来线程之间返回 ,yiled的线程 进入下面esle if 然后break 最后返回

  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
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
     
                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;
    }

讲一下addCount吧

不知道多少人跟我再没看增加size的方法直接一样 以为就是利用原子类的size或者加锁实现了size的++

果然大神的思路就是跟我这个大菜b不一样。言归正传

先说一下思路 利用一个CounterCell [ ] 数组和一个basecount(这个相当于基础的size) 当一个线程正在修改basecount的时候 其他线程去通过一个hash算法 算出再CounterCell [ ]中的位置 然后对对应位置利用cas修改对应位置的值 最后统计size的时候 遍历CounterCell [ ]和basecount加起来.
每一个CounterCell 里面又一个value 每次相当于也锁住的是数组对应的位置而不是锁住整个数组 这就是他并发量高的原因。

思路就是这样 思路明白了以后

接下来进入源码分析

   private final void addCount(long x, int check) {
      //x传入进来的是1  check就是前面的
   																		//bincount
		//这里就是前面说数组
        CounterCell[] as; long b, s;
        //注意这里判断 第一次添加size counterCells肯定为空 如果counterCells不为空他就不会修改basecount了而是直接修改counterCells对应位置的值
        if ((as = counterCells) != null ||
        	//这里利用cas修改basecount的值 如果修改失败 也就是别的线程正在修改才会进入下面代码
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
     
            
            CounterCell a; long v; int m;
            boolean uncontended = true;
		
		//这里的意思是 如果CounterCell数组为空,或者说 CounterCell数组算出来下标对应位置为空
		//或者说 在算出下标对应位置利用cas修改失败 就会进入fulladdCount方法
		//ThreadLocalRandom.getProbe() & m这个代码每次算出来的值对于同一个线程都行一样的
            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();
        }
        //这里是没有走上面return的代码
        //check也就是前面的bincount  什么时候会小于0了
        // 当然是remove一个key的时候 这里是插入 只要前面没return 都会走这个代码
        //下面其实就是扩容
        if (check >= 0) {
     
            Node<K,V>[] tab, nt; int n, sc;
            //这里的sizeCtl在初始化的时候被修改成了 阈值也就是 平衡因子*容量 0.75*16=12
            //如果说s(也就是目前位置的总的size数目)大于了阈值 并且hash桶长度小于最大长度就扩容。
            //为什么用while不用if了因为 并发量高的情况下扩容完毕可能又满了又要继续扩容 我们这个线程又可以继续扩容
            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;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                //利用cas修改成一个非常小的值 用于统计帮助转移的线程数量
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                     //开始转移
                    transfer(tab, null);
                //重新统计总的size 
                s = sumCount();
            }
        }
    }

**

fullAddCount源码

**

  private final void fullAddCount(long x, boolean wasUncontended) {
     
        int h;
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
     
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }
        //默认数组冲突是false
        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) {
     
            	//这里就是判断 算出对应下标数组中的元素是否为空 这里是为空
            	//为空肯定就要放入一个CounterCell 对象进去嘛 所以就会修改cellBusy为1
            	//表示正在被使用其他线程不能修改 然后再对象位置new 一个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;
                }
                //这个参数与扩容有关 相当于拿来判断当前线程修改成功的次数
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                    //代表数组中所有元素都初始化过 就可以修改对应下标位置的值了 不是锁住整个数组而是锁住对应下标
                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);
            }
            //这里是CounterCell数组为空 代表还没被初始化,cellsBusy 为0代表没有线程
            //正在使用这个数组 于是就用cas修改为1 不准别的线程再来初始化了
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
     
                boolean init = false;
                try {
                                // Initialize table
                    if (counterCells == as) {
     
                    //开始初始化数组
                        CounterCell[] rs = new CounterCell[2];
                        //并且把对应位置的值修改成x也就是1 然后就退出循环了。因为值已经插入进去了
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
     
                //把数组修改为没有正在使用状态
                    cellsBusy = 0;
                }
                //退出循环
                if (init)
                    break;
            }
            //如果数组正在初始化 则尝试修改basecount的值  肯定不会傻傻等待把
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

讲扩容 transfer(tab, null);

先讲一下扩容的总体思路:
扩容的时候 会算出一个步长 根据cpu的核心数量来算,最小为16 现在为了方便 假设 数组数量为4 步长为2 那么就会通过计算算出A线程的步长是2开始位置是下标是3 2 默认是从数组从右像左扩容的 B线程来了算出的位置就是1 0 然后把对应位置转移过去,并且会把对应位置更新为一个forwardingNode 他的hash值为1 这也是put操作中判断正在扩容并且帮助扩容的重要标志。

transferIndex=4

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
     
        int n = tab.length, stride;
        //这里就是算步长,根据cpu核心数 最小为16
        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;//这个变量代表了当前线程是否需要继续扩容
 			//假设当然map容量为4 步长为1 只要A线程扩容每次扩容一个 那么下一次肯定是要继续扩容的
 			//但是也有可能有多个线程同时扩容 我扩容完了 其他线程把整个也扩容完了就不需要前进,继续扩容了后面会修改为false    


			//判断当前线程的工作是否完成 跟前面一样 假设只要A线程再扩容 那么我肯定还需要再继续工作,
			//但是如果其他线程正在扩容其他的 我就完成了 就修改完true
        boolean finishing = false; // to ensure sweep before committing nextTab	

		//这里的I和bound带变了 每次扩容的边界
        for (int i = 0, bound = 0;;) {
     
            Node<K,V> f; int fh;
            while (advance) {
     
                int nextIndex, nextBound;
                //如果i减一以后还是>=bound说明还在步长内不需要继续前进扩容
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
     
                //下一个边界都小于=0了说明也不继续前进了
                    i = -1;
                    advance = false;
                }
                //第一次回进入到这里 假设map容量为4 步长为2
                //TRANSFERINDEX再前面看到是等于n=4的
                //nextIndex=TRANSFERINDEX=4
                //nextBound=4-2=2;
                //i=nextIndex=1=3
                //同时cas修改 TRANSFERINDEX为nextBound
                //现在明白了把 就是这样算的  
                //假设其他线程也修改 他们获得的TRANSFERINDEX是2 注意看上面的else if 这时候
                //来了一个线程B获取到的TRANSFERINDEX就是2 算出的i和bound就是1 和0  
                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;
                }
                //判断sizectl 是否等于最开始那个初始值 等于初始值代表所有线程扩容完毕
                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
                }
            }
            //如果说当前位置为空 说明不需要转移 之间设置fwd 防止其他线程这时候插入新值
           //扩容的时候肯定不能插入嘛
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
                //跟null一样的道理
            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;
                            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;
                        }
                        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;
                        }
                    }
                }
            }
        }
    }

put方法里面的helpTransfer

再前面put方法里面说过了这个方法用来帮助扩容 再前面也说过 扩容的时候回把sizeCtl改成非常小的一个值用来统计帮助线程扩容数量

 final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
     
        Node<K,V>[] nextTab; int sc;
        //这里是扩容的时候修改的ForwardingNode 里面有新的数组
        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) {
     
                   //同样扩容的时候会把sizeCtl修改成一个非常小的值
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                    //这里就用cas来统计如果线程帮助扩容就 扩容线程数量+1
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
     
                    transfer(tab, nextTab);//真正帮助开始扩容 这个方法前面已经讲过
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

写的不好的地方请各位大神 指出,制作不易 每次写 4 5个小时 如果觉得还行 麻烦给个赞

你可能感兴趣的:(java)