java集合-ConcurrentHashMap【jdk1.8】


前言

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的逻辑,经历四重大小判断,直到不相等或者全部相等为止。
    1. key值的hash(int类型)比较
    2. Comparable.compareTo()【这两个方法前者获取Comparable类对象,若获取成功,说明key是Compable类,第二个方法就直接调用compareTo()】
    3. tieBreakOrder()与HashMap一样,拥有两个比较功能:
      1. 比较类型名称【String比较】
      2. 比较原生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

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表示这趟循环已经完成了创建,跳出此时循环,再次进入其他的逻辑。

这个方法经常采用标记位 + 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附上去了事
      【这样的做法,造成了两个后果:
  1. 这个线程完成数组初始化的同时也就完成了加size的任务
  2. 其他线程使用这个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:下一轮左边界

  1. 计算步长stride:stride与cpu的核心数有关,只有多线程情况下赋值为表长 / cpu核心数【很好理解,相当于若所有cpu核心都在帮助扩容,那就每个线程平分来进行认务】;否则设置为单线程任务量
  2. nextTab只会在扩容中有值,扩容结束后为空,因此判断他为空可以说明当前未开启扩容。
  3. 创建新表,表长为原来大小的两倍。
  4. 设置sizeCtl为最大值,用于计算参与扩容的线程数量。
  5. 初始化transferIndex为老表长
  6. 初始化一个ForwardingNode的node节点,用于之后替换原table的node【原理是将hash设为-1,在之前的put中有过,当hash < 0,就过来helpTransfer()】
  7. 初始化扩容边界
  8. 当advance标志为true,表明还需要进行扩容
  9. 从右向左,若i 已经移动到了bound的左边,说明[bound, i]的区间已经不存在,扩容完毕【finishing == true也是】,跳出扩容循环
  10. 当transferIndex <= 0,说明任务分配完毕,无需参与扩容
  11. 在下一个else if中,初始化下次扩容的左边界:nextBound, 只要扩容区间还能大于一个步长,就算出正常的左边界,若不能大于,直接将nextBound设为0,使得必定从第一个if跳出。此时是一个cms操作,这个赋值未必可以成功,看接下来的代码
  12. 迭代i和bound,advance设为false,跳出当前while循环
  13. 接下来的几个条件都说明i已经超出了原来表的范围,即扩容结束了,将全局变量设置一下,结束程序【i之所以会大于等于n,是因为下面的那个if中的结束操作会将i赋值为n】
  14. cms操作sc,将sc减一;因为初始时sc设置为:rs << RESIZE + 2,且每个线程进入helpTransfer()时都会将sc + 1,且在这里执行cas减一,因此若此时也等于这个值,则说明当前就是最后一个线程了,将finishing置为true,i设为n,表示循环结束【之所以advance也设置为true,是为了能够保证在下次循环时还能进入那个while循环,之后按照条件赋值退出【就是上一个if finishing中的全局变量赋值】】
  15. 若advance为false,且当前没有扩容结束,利用cas取出要操作的那个桶位【以前一直叫数组下标的位置,老长了,感觉这个名字比较装逼】,若取出成功,且该桶位为空,就替换为transfer的标志节点fwd,若赋值成功,advance设为true

casTabAt()表示对tab这个备操作对象,当tab[i] == null时,将其赋值为fwd

  1. 若该桶位不为空,且放置了fwd,说明已经有其他线程transfer过了,advance设为true;
  2. else就代表桶位不为空,且其中的元素还是普通的Node没有经过搬迁,于是当前线程帮助搬迁:
  3. 对该桶位上锁,执行扩容逻辑:

fh > 0:根据刚开头的分析,为了区分节点,他将TreeBin的hash值设为负数,而此时fh > 0,说明是普通链表节点的插入逻辑:

  1. 第一步还是检查上锁的时候目标有没有被修改;
  2. 几个变量:

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桶结构。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值