JUC集合类 ConcurrentHashMap源码解析 JDK8

文章目录

  • 前言
  • 常量
  • 成员
  • 节点类
  • 构造器
  • put 插入操作
    • 加锁情况
    • 红黑树的binCount固定为2
    • 返回情况
    • spread
  • initTable
  • helpTransfer
    • resizeStamp
    • sizeCtl的低16bit
    • 退出循环的条件
  • treeifyBin
    • tryPresize
  • addCount
    • 计数部分
    • 计数部分结束时
    • 扩容部分
    • CAS失败影响扩容
  • fullAddCount
    • wasUncontended的作用
    • collide的作用
    • cellsBusy相当于独占锁
    • 不用担心把x加到旧的数组成员上去
  • transfer
    • 领取任务,执行任务的过程
    • 最后完成任务的线程,进行sweep检查
    • 哈希桶分离的处理
      • 链表分离处理
      • 红黑树分离处理
      • 扩容不影响并发的读操作
    • 扩容期间,成员的变化
    • 最后的sweep检查是没有必要的
  • get 查找操作
    • ForwardingNode的find
  • remove 删除操作
  • size 获得大小
  • clear 清空操作
  • 迭代器
    • 过程分析
  • 视图
  • ReservationNode
    • computeIfAbsent
    • compute
    • ReservationNode节点存在的必要性
  • Unsafe的使用
  • 总结

前言

ConcurrentHashMap是一种支持并发操作的HashMap,并发控制方面,它使用更小粒度的锁——对每个哈希桶的头节点加锁。虽然这样使得效率更高,能让读写操作最大程序的并发执行,但也造成了读写操作的一致性很弱,比如size()返回的大小可能已经与真实大小不一样,比如clear()调用返回后Map中却拥有着元素。

JDK8的ConcurrentHashMap并不是完美的,在阅读源码之前,建议先看看ConcurrentHashMap有些什么bug,以免在这些地方钻牛角尖。

JUC框架 系列文章目录

常量

    static final int MOVED     = -1; // hash for forwarding nodes
    static final int TREEBIN   = -2; // hash for roots of trees
    static final int RESERVED  = -3; // hash for transient reservations

ConcurrentHashMap中的正常节点的hash值都会是>=0的数,但还有三种特殊节点,它们的hash值可能是上面的三个值。

  • MOVED,代表此节点是扩容期间的转发节点,这个节点持有新table的引用。
  • TREEBIN,代表此节点是红黑树的节点。
  • RESERVED,代表此节点是一个占位节点,不包含任何实际数据。

成员

//存放Node的数组,正常情况下节点都在这个数组里
transient volatile Node<K,V>[] table;
//一个过渡用的table表,在扩容时节点会暂时跑到这个数组上来
private transient volatile Node<K,V>[] nextTable;
//计数器值 = baseCount + 每个CounterCell[i].value。所以baseCount只是计数器的一部分
private transient volatile long baseCount;
//1. 数组没新建时,暂存容量
//2. 数组正在新建时,为-1
//3. 正常情况时,存放阈值
//4. 扩容时,高16bit存放旧容量唯一对应的一个标签值,低16bit存放进行扩容的线程数量
private transient volatile int sizeCtl;
//扩容时使用,平时为0,扩容刚开始时为容量,代表下一次领取的扩容任务的索引上界
private transient volatile int transferIndex;
//CounterCell相配套一个独占锁
private transient volatile int cellsBusy;
//counterCells也是计数器的一部分
private transient volatile CounterCell[] counterCells;
  • table。存放节点的数组,只有第一个插入节点时才初始化,默认容量为16。扩容会让容量变成2倍,即保持为2的幂。
  • nextTable。扩容时需要使用,暂存新table,扩容完毕时,会把新table赋值给table
  • sizeCtl。如果使用ConcurrentHashMap的无参构造器,那么它默认为0。其他的值,它都有不同的含义:
    • 数组没新建时,暂存容量。第一个插入节点时,会按照这个容量新建数组。
    • 数组正在新建时,为 − 1 -1 1
    • 正常情况时,存放阈值。阈值 = 0.75 * 容量。
    • 扩容时,高16bit存放旧容量唯一对应的一个标签值,低16bit存放进行扩容的线程数量
  • transferIndex。扩容时每个线程通过CAS修改该成员,来领取扩容任务。
  • baseCountcounterCells。是计数器的实现依赖,类似于LongAdder,它利用ThreadLocalRandom的探针机制来避免频繁的CAS失败,从而减少了因CAS失败而产生的自旋。

节点类

JUC集合类 ConcurrentHashMap源码解析 JDK8_第1张图片

  • 如果哈希桶的结构是单链表,那么每个节点的类型为Node。其中val域是volatile的,因为可以执行替换操作。next也是volatile的,因为可以执行删除操作,导致链表结构发生变化。
  • 如果哈希桶的结构是红黑树,那么存放在数组索引处的节点类型为TreeBin,但它不是真正的红黑树的root节点。它的root成员才是红黑树根节点。也就是说,TreeBin封装了TreeNode,这是有好处的,因为红黑树随着平衡操作,根节点随时可能发生变化,但用TreeBin进行封装,可以让数组成员不会发生变化。
    • 真正的红黑树节点类型为TreeNode。只有当数组容量>=64且单个链表长度>=8,才会让这个链表转换为红黑树。
  • ForwardingNode是扩容期间的转发节点,这个节点持有新table的引用。
  • ReservationNode是占位节点,不包含任何实际数据。在computeIfAbsentcompute方法中使用。

构造器

    public ConcurrentHashMap() {
    }

默认初始化,什么也不干。

    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1))
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

首先要知道,这里的initialCapacity + (initialCapacity >>> 1) + 1其实是一个bug,正确的做法应该是下面这个构造器的做法。

    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        //获得cap的正确做法
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

put 插入操作

put方法既是难点也是重点。难点在于它涉及到很多操作:初始化数组、插入动作、计数增加、扩容。

    public V put(K key, V value) {
        return putVal(key, value, false);
    }

putValputputIfAbsent两个public方法的真正实现。

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());//根据原始hash值,获得处理后的hash值
        int binCount = 0;//将代表一个哈希桶内节点数
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //如果发现table还没初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果发现索引所在元素为null,那么就CAS该null元素
            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
            }
            //执行到这里,索引所在元素不为null
            //如果发现元素的hash值为MOVED
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);//帮忙扩容转移
            //如果发现元素的hash值为其他情况
            else {
                V oldVal = null;
                synchronized (f) {//必须先锁住该数组元素
                    if (tabAt(tab, i) == f) {//锁住后,需要检查它是否还在这个索引上
                        //说明f是一个普通节点
                        if (fh >= 0) {
                            binCount = 1;//f是链表的头节点,把f自己先考虑进去
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //发现有重复key,根据onlyIfAbsent决定是否替换,
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;//不管接下来替不替换,oldVal都会被赋值
                                    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;//binCount不会算上新增的节点
                                }
                            }
                        }
                        //如果f是一个TreeBin
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;//保持为2
                            //很明显,putTreeVal有两种可能:
                            //1. 新建节点,返回值为null
                            //2. 找到重复节点,但不执行替换操作,只是返回重复的节点
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;//不管接下来替不替换,oldVal都会被赋值
                                if (!onlyIfAbsent)
                                    p.val = value;//替换操作
                            }
                        }
                    }
                }
                //不等于0,说明执行了新增操作,或替换操作(可能替换没有执行,因为onlyIfAbsent)
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);//将链表转换成红黑树
                    if (oldVal != null)//说明找到了重复节点,至于替换与否,根据onlyIfAbsent来
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);//size增加1,并可能扩容table
        return null;//说明肯定是新建操作
    }

加锁情况

putVal是写操作,当定位到某个非空的哈希桶,需要对这个哈希桶的头节点synchronized加锁。相反,当定位到的是空的哈希桶,则只需要CAS修改就好了。

  • 对于链表来说,头节点不会变,所以对它加锁好使。
  • 对于红黑树,TreeBin节点封装红黑树的root节点(将root节点作为它的成员),这样,即使红黑树因为平衡操作而改变root,也只是改变了TreeBin节点的一个成员而已。所以对TreeBin节点加锁好使。

红黑树的binCount固定为2

之所以红黑树的binCount固定为2,是因为:

  • 已经检测到该哈希桶是红黑树了,就不用再树化(binCount >= TREEIFY_THRESHOLD)。
  • 传入addCount(1L, binCount)的第二个参数为2,保证之后能进行扩容检查。

返回情况

  • 返回null,说明putVal执行的新建节点的操作。
  • 返回非null值,说明putVal检测到了重复节点,至于替换与否,根据onlyIfAbsent决定。
    • 如果onlyIfAbsent为false,将替换。否则,不替换。

spread

    static final int HASH_BITS = 0x7fffffff; // 后面有31个1,相当于符号位固定为0,

    static final int spread(int h) {
    	//低16位将受到高16位的扰动,& HASH_BITS后肯定为正数
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

initTable

该函数用来初始化null的table,它可能会被两个线程并发执行。

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            //如果有其他线程正在初始化(sizeCtl为-1)
            //或扩容(sizeCtl为其他负值)
            //那么让出cpu,下一次循环可能还是进入这个分支,所以将会是自旋的过程
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            //如果大于等于0,说明sizeCtl暂存着容量呢
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//先尝试修改sizeCtl
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//如果sc不为0,那么肯定是一个合理的容量
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);//这里是阈值,n * 0.75
                    }
                } finally {
                    sizeCtl = sc;//将sizeCtl设置为阈值
                }
                break;
            }
        }
        return tab;
    }
  • sizeCtl为-1,代表正在初始化table。
  • sizeCtl设置为-1,相当于一把锁,专门用来初始化table的锁。
  • 初始化完毕,sizeCtl将设置为阈值,即容量*0.75。相当于释放锁。

helpTransfer

在发现table数组元素是一个ForwardingNode时(MOVED),调用此函数帮忙扩容,但此函数只是尝试获得许可(U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)),真正的扩容操作还是由transfer完成的。

    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) &&//检查f是否还是ForwardingNode
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {//从f的成员获得nextTab
            //一旦进入此分支,返回的肯定是新table

            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)) {//尝试增加1的线程数
                    transfer(tab, nextTab);//执行完transfer后则退出
                    break;
                }
            }
            return nextTab;
        }
        //上面if分支没有进入,说明扩容在进入if分支前就已经完成了,table成员已经是扩容后的新table
        return table;
    }

resizeStamp

参数n是table的容量,容量肯定是2的幂。该函数根据一个容量n,得到与n唯一对应的一个标签值,这个标签值只需要16bit存储。

    private static int RESIZE_STAMP_BITS = 16;

    static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

我们知道,table的最小容量是16即 2 4 2^4 24,最大容量是 2 30 2^{30} 230。而numberOfLeadingZeros用来计算出第一个1前面的0的个数。

  • 2 4 2^4 240000 0000 0000 0000 0000 0000 0001 0000,所以numberOfLeadingZeros返回27。
  • 2 30 2^{30} 2300100 0000 0000 0000 0000 0000 0000 0000,所以numberOfLeadingZeros返回1。
  • 所以,Integer.numberOfLeadingZeros(n)的取值范围其实就是1~27。
  • 2 4 2^4 24 2 30 2^{30} 230,每种可能的容量,Integer.numberOfLeadingZeros(n)都不一样。这就是唯一对应

1 << (RESIZE_STAMP_BITS - 1)就是把1左移15位,所以就是1000 0000 0000 0000。而最大的27都不能占据到第16个bit,所以与1000 0000 0000 0000与一下,不会影响原数的有效bit,只是会把第16个bit置1。

扩容时,resizeStamp的返回值将作为sizeCtl的高16bit使用,这样sizeCtl的符号位肯定为1,此时sizeCtl肯定为负数。

并且由于最大的27都不能占据到第16个bit,那么resizeStamp返回值的bit不可能都为1,那么扩容时,sizeCtl肯定不能为-1,因为-1是每个bit都为1,所以说明sizeCtl为-1是能唯一代表当前在初始化中的。

sizeCtl的低16bit

sizeCtl的低16bit用作线程扩容的许可,有点像是Semaphore的作用。我们把低16bit当成一个无符号整数来看待即可,当对sizeCtl加1时,sizeCtl的低16bit就会加1;当对sizeCtl减1时,sizeCtl的低16bit就会减1。我们称低16bit的这个无符号整数,为sizeCtl的线程数,或者许可数。

每一个线程来扩容时都需要获得许可:

  • 第一个开始扩容的线程,进入transfer前需要获得2个许可。实际动作为,sizeCtl + 2。
  • 之后开始扩容的线程,进入transfer前需要获得1个许可。实际动作为,sizeCtl + 1。

但每个线程执行完transfer任务,归还许可时,动作都是sizeCtl - 1。这样,当最后一个线程归还许可后,sizeCtl的线程数必定为1。最后一个线程检测到sizeCtl为1时,会从尾到头再扫查一遍每个哈希桶。

之所以第一个开始扩容的线程需要获得2个许可,是因为作者希望在这 2 16 − 1 2^{16}-1 2161的可能状态中,将一种状态用作扩容结束前的中间状态,这种中间状态,作者将从尾到头再扫查一遍每个哈希桶是否都完成了转移。(具体看transfer函数讲解)

退出循环的条件

首先要知道,这里有个bug。sc == rs + 1这里,应该写成sc == (rs << RESIZE_STAMP_SHIFT) + 1sc == rs + MAX_RESIZERS同理。

                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;

上面这个分支进入,说明不需要当前线程帮忙扩容了。

  • (sc >>> RESIZE_STAMP_SHIFT) != rs,刚读取的sc的容量标签值和参数tab的容量标签值,说明在调用期间,table成员已发生变化,那自然是因为扩容。
  • sc == rs + 1,扩容结束前的中间状态。
  • sc == rs + MAX_RESIZERS,许可已经被领完。
  • transferIndex <= 0,transfer任务已经被领完。(具体看transfer函数讲解)

treeifyBin

该函数首先判断操作应该是扩容,还是树化,但真正的树化操作交给了TreeBin的构造器。

    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) {//树化也是写操作,需要加锁
                    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));//利用TreeBin构造器来树化
                    }
                }
            }
        }
    }

tryPresize

该函数判断需要扩容后,将开始扩容。扩容逻辑类似addCount的扩容逻辑。

    private final void tryPresize(int size) {//参数是旧数组的容量的二倍
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);//算出一个目标容量
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            if (tab == null || (n = tab.length) == 0) {//如果table为null,那么此时sizeCtl暂存着一个容量值
                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 = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            //执行到这里,table成员不为null
            //如果目标容量比现有容量小,或现有容量已经到达最大容量。就没有必要扩容了
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            //如果table成员还没变,此时判断需要扩容
            else if (tab == table) {
                int rs = resizeStamp(n);//获得容量标签值
                //如果sizeCtl已经小于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;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                //如果sizeCtl大于0,说明存的是阈值,说明扩容还没开始。由当前线程开始
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

addCount

统计ConcurrentHashMap的size的任务,交给了baseCount成员和counterCells数组成员。

  • counterCells为null时,baseCount的值就是size。
  • counterCells不为null时,baseCount加上每个counterCells的数组元素的值,才是size。

之所以引入counterCells数组,是因为如果每个线程都在CAS修改baseCount这一个int成员的话,必定会造成大量线程因CAS失败而自旋,从而浪费CPU。现在引入一个counterCells数组,让每个数组元素也来承担计数的责任,则线程CAS修改失败的概率下降了不少。

所以,当counterCells不为null时,一定要去优先修改counterCells的某个数组成员的值,而且由于利用了ThreadLocalRandom的probe探针机制来获得数组的随机索引,所以很大概率上,不同线程获得的数组成员是不同的。既然不同线程获得的数组成员不同,那么不同线程尝试CAS修改某个数组成员,肯定不会失败了,从而减小了线程的因CAS失败而导致的自旋。

了解了以上知识,我们再来看addCountfullAddCount的实现,就好懂多了。

//参数x是需要增加的数量。 check用来判断是否需要检测,如果检测到需要扩容,就扩容。
    private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;

        /*计数部分*/
        if ((as = counterCells) != null ||  //如果counterCells成员不为null,短路后面条件
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {//如果counterCells成员为null,但CAS失败
            CounterCell a; long v; int m;
            boolean uncontended = true;

            //进入下面这个分支很容易:
            //1. 如果as为null 2. 如果as的大小为0(这不可能,只是一种保护)
            //3. 如果as的随机索引位置上的元素还没初始化  4. CAS修改一个cell的值失败了
            //不进入这个分支很难:只有当 as不为null,且大小不为0,且随机索引位置元素不为null,
            //且修改这个cell元素的value成功了,才不会进入分支。
            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;
            }
            //执行到这里,说明 修改某个cell的value成功了,计数已经成功加上去了。
            
            //既然修改某个cell的value成功了,而且check参数还这么小,就直接返回,不用去做check动作了
            if (check <= 1)
                return;
            //如果需要check,那么算出当前的size
            s = sumCount();
        }
        //执行到这里,说明上面代码,要么CAS修改baseCount成功了,要么CAS修改某个cell的值成功了
        //而且s已经是当前map的映射数量了。

        /*扩容部分*/
        if (check >= 0) {//如果需要check
            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);//按照当前容量n,得到与n唯一对应的一个标签值
                //如果正在扩容
                if (sc < 0) {
                    //1. 如果sizeCtl得到的容量标签值,与之前得到的不一样
                    //2. 如果扩容线程数为1,说明扩容结束(因为第一个线程,设置线程数量为2,最多增长到MAX_RESIZERS)
                    //3. 如果扩容线程数为MAX_RESIZERS,说明可以帮忙扩容的线程的数量到达上限
                    //4. 如果nextTable为null,说明扩容结束
                    //5. 如果transferIndex <= 0,说明transfer任务被领光了,所有的哈希桶都有线程在帮忙扩容
                    //以上情况,都说明当前线程不需要去扩容操作了
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    //如果看起来可以帮忙扩容,那么尝试增加一个线程数量
                    //上面分支每个条件都为false才可能执行到这里,说明nt肯定不为null
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        //修改成功,相当于拿到许可,开始扩容
                        transfer(tab, nt);//nt不为null
                }
                //如果sc大于0(这里其实不可能等于0),说明当前table没有在扩容,
                //当前线程作为第一个线程,所以要设置线程数为2
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();//再次计算size,开始下一次循环。有可能下一次循环,还会继续扩容(如果s >= (long)(sc = sizeCtl))
            }
        }
    }

我们把addCount的逻辑分为计数部分和扩容部分。

计数部分

计数部分的if分支有可能根本没有进入,因为当前counterCells成员为null,且修改baseCount成功了。此时计数部分已经完成了任务,并将s局部变量设置为了map的size。

现在考虑进入计数部分的if分支,此时有两种情况:

  • counterCells成员不为null,短路后面条件。这种情况,接下来会去尝试CAS修改某个cell的值。
    • 如果CAS修改某个cell的值失败了,那么会去执行fullAddCount(x, uncontended)然后直接return掉。
    • fullAddCount(x, uncontended)传入的uncontended参数此时必为false。(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)失败)
  • counterCells成员为null,但CAS修改baseCount失败。这种情况一定去执行fullAddCount(x, uncontended)然后直接return掉。因为as == null成立,直接短路后面条件。
    • fullAddCount(x, uncontended)传入的uncontended参数此时必为true。 (boolean uncontended = true

所以,当调用fullAddCount(x, uncontended)时,一定是因为之前,尝试CAS修改baseCount失败、或者CAS修改某个cell的值失败了。

fullAddCount(x, uncontended)传入的uncontended参数为false代表,CAS修改某个cell的值失败了。

计数部分结束时

当计数部分结束时,s局部变量的值就已经是map的size了。分为两种情况:

  • 计数部分的if分支根本没有进入,此时counterCells成员为null,所以只需要增加baseCount就好。
  • 计数部分的if分支进入了,而且CAS增加某个cell的值成功了,然后依靠s = sumCount()算出来。因为这种情况,计数分布在baseCountcounterCells数组上。

扩容部分

  • 循环将一直执行,直到扩容结束。扩容中时,条件s >= (long)(sc = sizeCtl)一定成立,因为扩容时sizeCtl为负数。
  • 注意,第一个开始扩容的线程,会将线程数设置为2,线程数最大可以增长到MAX_RESIZERS。之所以这样,是因为线程数为1,用来代表扩容结束。
  • 调用transfer去做真正的扩容。

CAS失败影响扩容

从以上分析可知,不管是CAS修改baseCount失败、或者CAS修改某个cell的值失败了,只要是失败了,之后都会执行fullAddCount然后直接return,从而不去执行扩容部分。

只有CAS成功了,才有可能去调用扩容部分的代码。

这样的做法,可能会导致,该扩容的时候不会扩容。因为毕竟只有CAS直接成功的线程,才可能扩容。比如这种场景,线程A CAS成功,此时大小为阈值-1;然后线程B CAS失败,此时大小刚好为阈值,但由于失败,不会去扩容;只有等到第3个线程到来,才可能去扩容了。

fullAddCount

addCount函数里CAS修改baseCount失败、或者CAS修改某个cell的值失败了,会调用到fullAddCount。该函数保证能够将x加到baseCount或某个cell上去。

    private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        //线程第一次调用fullAddCount,肯定会进入以下分支,因为只有localInit后,probe才不可能为0
        //之后调用fullAddCount,就不可能进入这个分支。
        if ((h = ThreadLocalRandom.getProbe()) == 0) {//探针为0,说明localInit从来没有调用过
            ThreadLocalRandom.localInit();      // 先初始化再说
            h = ThreadLocalRandom.getProbe();   // 这句get到的就肯定不为0了
            wasUncontended = true;
        }
        boolean collide = false;
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            //如果counterCells数组不为null,那么当然优先在某个数组元素增加x
            if ((as = counterCells) != null && (n = as.length) > 0) {
                //探针取余后的下标的元素为null
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) { //这个cellsBusy相当于一个独占锁的state,为0说明可以获得锁 
                        CounterCell r = new CounterCell(x); // 乐观地创建,因为假定获得锁后,该索引位置还是为null
                        if (cellsBusy == 0 &&
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//尝试获得锁
                            boolean created = false;
                            try {  // 获得锁后,还是需要检查该索引位置是否还是为null
                                CounterCell[] rs; int m, j;
                                if ((rs = counterCells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {//该索引位置为null,接下来赋值
                                    rs[j] = r;
                                    created = true;//赋值成功
                                }
                            } finally {
                                cellsBusy = 0;//最终无论如何,都要释放锁
                            }
                            if (created)//如果赋值成功,那么该函数任务完成
                                break;
                            continue;  //执行到这里,说明赋值没有成功,因为获得锁后,发现该索引位置却不为null了
                        }
                    }
                    collide = false;
                }
                //执行到这里,说明探针取余下标的元素不为null
                
                //从addCount的逻辑来看,只可能addCount里CAS修改某个cell失败了,
                //才会导致addCount调用此函数的wasUncontended为false
                else if (!wasUncontended)       // 既然之前的探针冲突了,那么就执行advanceProbe获得下一个探针
                    wasUncontended = true;      
                //CAS修改当前探针指向的数组元素,如果成功了break
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                //如果counterCells更新了,或者数组大小大于CPU个数,短路后面所有if分支,优先移动探针
                else if (counterCells != as || n >= NCPU)
                    collide = false;   
                //短路后面if分支,并消耗掉collide       
                else if (!collide)
                    collide = true;
                //如果没有被前面短路,将进入扩容分支
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//前提是获得锁成功
                    try {
                        if (counterCells == as) {//需要再次检查counterCells成员没有变化
                            CounterCell[] rs = new CounterCell[n << 1];//扩容二倍
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];//重复利用旧的成员,新位置为null
                            counterCells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;   // 扩容完事,不移动探针
                }
                h = ThreadLocalRandom.advanceProbe(h);
            }
            //执行到这里,说明之前检测到counterCells数组为null
            //cellsBusy代表是否可以进行初始化操作
            else if (cellsBusy == 0 && counterCells == as &&//这里counterCells和as都为null
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//将初始化许可设置为1
                boolean init = false;
                try {  //执行初始化操作
                    if (counterCells == as) {
                        CounterCell[] rs = new CounterCell[2];//建立大小为2的数组
                        rs[h & 1] = new CounterCell(x);//探针取余得到下标,只设置一个数组元素(lazy init)
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            //执行到这里,说明之前检测到counterCells成员为null,且没有抢到初始化操作的。
            //那就退而求其次,去设置baseCount好了。
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

wasUncontended的作用

  • wasUncontended为true,代表线程修改某cell无竞争。
  • wasUncontended为false,代表线程修改某cell有竞争。

不需要关心参数wasUncontended传进来为true的情况,因为这种情况不会造成什么影响。

我们已知,当fullAddCount的参数wasUncontended为false的情况,一定是因为addCount函数里CAS修改某个cell的值失败了(即有竞争)。但这个线程需要分为两种情况:

  • 这种线程第一次进入fullAddCount,则一定会进入if ((h = ThreadLocalRandom.getProbe()) == 0),然后将wasUncontended设置为true。
    • 这种情况说明之前addCount函数里通过ThreadLocalRandom.getProbe()获得的探针为0,0是作为探针没有初始化的默认值的,也就是说,之前addCount函数里CAS修改某个cell的值失败,很可能是因为没有获得到正常非零的探针值才导致的。所以,获得非零探针值后,就将wasUncontended设置为true,代表之前的线程竞争其实不算事。
  • 这种线程第二次(或以后)进入fullAddCount,则不会进入if ((h = ThreadLocalRandom.getProbe()) == 0)wasUncontended不会被提前修改掉。
    • 这种情况说明之前addCount函数里通过ThreadLocalRandom.getProbe()获得的探针为非零值,这是一个正常的探针值。正常的探针值,因为线程竞争导致CAS失败了,说明探针需要移动了。
                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;
  • wasUncontended为false时,这个变量开始起作用。简单的说,它是用来短路下一个if分支的,包括后面的所有if分支。
  • wasUncontended为false时,代码将不会去U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)尝试修改某个cell的值。然后移动探针。

collide的作用

                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//扩容if分支
                	...
                	continue;
                }
                h = ThreadLocalRandom.advanceProbe(h);
  • 当collide为false时,当前循环它会短路后面的扩容if分支,但下一次循环则不会短路了。
  • 当collide为true时,当前循环它不会短路后面的扩容if分支。
  • 扩容if分支的最后一句是continue,说明一次循环中,要么是执行扩容操作,要么是移动探针。二选一,因为这种操作执行后,线程再去修改cell的值就更可能成功了。
  • 当然,如果U.compareAndSwapInt(this, CELLSBUSY, 0, 1)失败了,则没法进入扩容if分支,只好移动探针。

再来看一下collide的赋值情况:

  • boolean collide = false的默认值为false,说明默认情况下,优先移动探针。
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {            // Try to attach new Cell
						...
                    }
                    collide = false;
                }
  • 如果在尝试给null的数组成员赋值时,锁已经被别人持有,则collide赋值为false,优先移动探针。
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
  • 如果发现counterCells数组成员已经更新(必然是因为扩容,所以有了更大的位置可以赋值),或数组大小已经大于等于CPU数量(没必要优先扩容,因为理论上每个线程都有一个独有的数组成员了),则collide赋值为false,优先移动探针。
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//扩容if分支
                	...
                	collide = false;
                	continue;
                }
  • 在扩容if分支的最后,也会collide赋值为false,优先移动探针。

cellsBusy相当于独占锁

cellsBusy相当于独占锁的state,当它为1时,代表锁被某个线程持有。当线程持有锁时,可能扩容counterCells数组,也可能对某个null的数组成员赋值。

不用担心把x加到旧的数组成员上去

                            CounterCell[] rs = new CounterCell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];//重复利用

不用担心把x加到旧的数组成员上去,从而导致加的数字白加了。因为从扩容逻辑来看,旧的数组成员都被重复利用了。

transfer

transfer是真正的扩容实现,从上文已知它有这么几种调用过程:

  • putVal ⇒ helpTransfer ⇒ transfer。这种过程,当前线程肯定不是第一个获得扩容许可的那个线程。
  • putVal ⇒ addCount ⇒ transfer。这种过程,当前线程可能是第一个获得扩容许可的那个线程。
  • putVal ⇒ treeifyBin ⇒ tryPresize ⇒ transfer。这种过程,当前线程可能是第一个获得扩容许可的那个线程。
  • 第一个获得扩容许可的线程将执行U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2),将线程数量从0设置为2,且传递给transfer的第二个参数为null,代表nextTable还没初始化。
  • 之后获得扩容许可的线程将会把线程数增加1,且传递给transfer的第二个参数不为null,传递的就是nextTable。

transfer可以被多个线程同时调用,一起完成扩容的工作。而多线程一起工作,就意味着任务需要划分,而且各个线程不要相互干扰。

我们先来看看单线程的HashMap是如何扩容的:

  1. 新建一个2倍容量的新数组。
  2. 旧数组的每个哈希桶,会进行哈希桶分离,分离低桶和高桶。低桶放到新数组的i槽位上去,高桶放到新数组的i + n上去。

先简单解释下为什么这么分离:
在这里插入图片描述
PS:这个图是我以前分析HashMap源码的博客图,大家关注一下这个分离原理就行。

  • 假设旧容量是0b1000016,那么可能的table下标范围为0b0000 - 0b1111,即能影响到元素所在table下标的bit只有后4位bit0b????
  • 假设有四个元素,它们的hash值的最后4位bit都是XYZQ,由于当前容量16的限制,它们会被放置到同一个哈希桶(table下标为0bXYZQ)里。
  • 现在resize里扩容后,新容量升为0b10000032,所以现在能影响到元素所在table下标的bit只有后5位bit0b?????,但相比之前,只有右起第5位bit可能发生变化。
  • 所以,如果这个关键bit为0,那么元素还是处于原table下标,如果这个关键bit为1,那么元素处于 原table下标+旧容量 的新下标。
  • 图中可见,原下标与新下标的相差值,刚好就是旧容量0b10000即16。
  • 图中通过颜色来表示不同的元素,注意HashMap链表分离后,它们也能保持之前的相对位置。但ConcurrentHashMap却不一定会保持原有顺序了,之后可以看到。

既然每个哈希桶的元素在扩容后,要么留在 原table下标,要么留在 原table下标+旧容量 的新下标。那么说明分别处理不同哈希桶的两个线程是不会互相影响的。这样,就说明可以按照哈希桶为单位来划分transfer任务了。

但是线程调度也是有消耗的,所以领取一次任务肯定不可能只处理一个哈希桶。所以,我们根据CPU数算出来一个stride,但这个stride至少为16。这样,每个线程来领取到的任务就是:连续stride个的哈希桶。

而领取任务依赖的成员则是transferIndex,如果CAS修改transferIndex从当前值current成功改成current-stride,那么当前线程领取到的任务就是:[current-stride, current-1]

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;//stride为步长,代表线程一次性转移多少个哈希桶
        //stride可能等于 n ÷ 8 ÷ NCPU,但最小也得是MIN_TRANSFER_STRIDE
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; 
        if (nextTab == null) {    // 为null说明,nextTab还没初始化
            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;//transferIndex成员初始为容量,但作为索引来说,这是个不可能的索引
        }
        int nextn = nextTab.length;//所以nextn是n的二倍
        //初始化ForwardingNode节点,它持有新table的引用,旧table的一个哈希桶完全转移后,
        //将用fwd替换该哈希桶,作为占位符使用,表明该哈希桶已经转移
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // 检测到每个哈希桶都转移后,还需要从尾到头扫查一遍,是不是每个节点都是ForwardingNode了
        
        //循环将不断领取stride个哈希桶作为任务,范围则为[bound,i],自然i-bound+1 = stride(只要不是最后一次领任务)
        //从i遍历到bound,每次遍历转移一个哈希桶到新table
        for (int i = 0, bound = 0;;) {
            //f 当前遍历的哈希桶的头节点;  fh  f的哈希值
            Node<K,V> f; int fh;
            /*移动遍历指针部分*/
            while (advance) {//为true代表当前处理的哈希桶已经处理完毕,需要移动i和bound了
                int nextIndex, nextBound;//nextIndex是i下一个可能的值,nextBound是bound下一个可能的值
                if (--i >= bound || finishing)//移动遍历指针i,只要还在[bound,i]范围内,短路后面
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {//上面发现超过bound范围了,但没有任务可以领取了
                    i = -1;
                    advance = false;
                }
                //尝试领取stride个哈希桶作为任务
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    //如果领取任务成功,更新[bound,i]
                    bound = nextBound;
                    i = nextIndex - 1;//右边界总是作为一个不在范围内的索引,所以需要减1(最开始transferIndex为容量)
                    advance = false;
                }
                //advance = false后,除非设置回true,否则不会进入该循环了
            }

            /*处理当前遍历哈希桶部分*/
            //进入该分支说明所有的哈希桶都被领取完,但不一定当前都处理完毕了
            if (i < 0 || i >= n || i + n >= nextn) {//其实只有第一个条件能成立,而且肯定是因为i = -1
                int sc;
                if (finishing) {//sweep检查的收尾工作
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //既然任务都被领取完,而且当前线程自己的任务也都做完了,那当前线程就可以归还许可了
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //如果不是最后一个归还许可的线程,直接return,因为不需要它们做sweep检查
                    //如果是最后一个归还许可的线程,不return,然后开始sweep检查
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // 从尾到头全部检查一遍
                }
            }
            //当前遍历哈希桶为null,那好办,设置为ForwardingNode即可
            else if ((f = tabAt(tab, i)) == null)
                //如果设置失败,下一次循环还是检查同一个哈希桶,但不一定进这个分支
                advance = casTabAt(tab, i, null, fwd);
            //当前遍历哈希桶已经是ForwardingNode,不用处理
            else if ((fh = f.hash) == MOVED)
                advance = true; // 设置为true,遍历指针将移动
            //如果遍历哈希桶是一个正常节点
            else {
                synchronized (f) {//锁住哈希桶的头节点
                    if (tabAt(tab, i) == f) {//需要再次判断f是否还为头节点
                        Node<K,V> ln, hn;
                        //一个哈希桶内的节点只可能分离到两个地方,一个是原地不动 i,一个是i + 旧容量
                        //称它们为低桶和高桶。分离前桶里既有低桶元素也有高桶元素,而lastRun则是从尾到头
                        //找到和最后一个元素连续同阶级的第一个元素。

                        //链表分离处理
                        if (fh >= 0) {
                            int runBit = fh & n;//要么为n,要么为0.分别代表处于high,还是low
                            Node<K,V> lastRun = f;
                            //该循环用来找到和最后一个元素连续同阶级的第一个元素。
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                //从第二个元素开始遍历,因为第一个元素肯定和自己同阶级
                                int b = p.hash & n;//算出当前遍历元素是low还是high
                                if (b != runBit) {//如果当前遍历元素和当前lastRun元素的阶级不同
                                    //替换为当前遍历元素
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {//找到的是低桶元素,将lastRun设置给ln
                                ln = lastRun;
                                hn = null;
                            }
                            else {//找到的是高桶元素,将lastRun设置给hn
                                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)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            //因为是头插法,所以ln 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;//lo为低桶head,loTail为低桶tail
                            TreeNode<K,V> hi = null, hiTail = null;//hi为高桶head,hiTail为高桶tail
                            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;
                                //hc为0,说明原哈希桶只有低桶元素,就不用再重新组织树结构了
                            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);//最后才把原槽位,赋值为fwd
                            advance = true;
                        }
                    }
                }
            }
        }
    }

前面的逻辑主要处理了nextTab参数为null的情况,主要的逻辑还是集中在for (int i = 0, bound = 0;;)循环里,将其分为:

  • 移动遍历指针部分
  • 处理当前遍历哈希桶部分

领取任务,执行任务的过程

  1. 刚进入for (int i = 0, bound = 0;;)循环,第一次执行 移动遍历指针部分,进入else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0)))分支(假设CAS直接成功,如果不成功,那就不停CAS直到成功),当前线程领取到的任务是[bound, i],设置advance为false,代表i暂时不需要前进。
  2. 进入 处理当前遍历哈希桶部分,处理完当前哈希桶后,设置advance为true,代表i需要前进了。
  3. 下一次循环,进入 移动遍历指针部分,进入if (--i >= bound || finishing)分支,--i就移动了指针,好处理下一个哈希桶。设置advance为false,代表i暂时不需要前进。
  4. 进入 处理当前遍历哈希桶部分,处理完当前哈希桶后,设置advance为true,代表i需要前进了。
  5. 重复3,4步骤,重复stride次后(把1,2也算一次),此时i == bound
  6. 下一次循环,进入 移动遍历指针部分,发现进入不了if (--i >= bound || finishing)分支(执行了--i,此时i + 1 = bound),且发现还有剩余任务可以领取(else if ((nextIndex = transferIndex) <= 0)分支没有进入,说明transferIndex大于0,还有任务可以领),就再次CAS修改transferIndex来领取任务,如果领取成功,那么又将重复以上所有步骤。

最后完成任务的线程,进行sweep检查

首先得讲一下流程。既然都是最后完成任务的线程了,说明任务已经被领取完了,所以此时transferIndex为0。

  1. 当前i == bound,正在执行 处理当前遍历哈希桶部分。
  2. 处理完当前范围最后一个哈希桶,进入while (advance)循环。
  3. if (--i >= bound || finishing)不成立,进入下一个分支。此时i + 1 = boundfinishing为false
  4. else if ((nextIndex = transferIndex) <= 0)分支进入,因为此时transferIndex为0。还设置了i = -1
  5. 进入 处理当前遍历哈希桶部分,然后进入if (i < 0 || i >= n || i + n >= nextn)分支,然后进入if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)分支。这里的CAS操作是用来归还许可的,因为当初调用transfer前获得了这个许可。由于当前线程是最后一个来归还许可的,所以归还之前许可数肯定为2,因为第一个调用transfer的线程设置的许可数为2。而非最后一个归还许可的线程,归还之前许可数肯定大于2。
    1. 非最后一个归还许可的线程,会进入if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)分支,然后直接返回,因为它们不需要进行sweep检查。
    2. 最后一个归还许可的线程,不会进入if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)分支,设置**finishing为true**,好进行sweep检查。遍历指针重新设置为n,因为是从尾到头全部扫查一遍。
    3. finishing为true将导致while (advance)循环只会进入if (--i >= bound || finishing)分支,即只会减小i。此时前面的条件--i >= bound成不成立都无所谓,只是为了--i移动指针而已。
  6. 然后当前线程在for (int i = 0, bound = 0;;)大循环里,先 移动遍历指针,再 检查当前遍历哈希桶。
  7. 最后一个大循环中,移动遍历指针后,i变成-1,又进入if (i < 0 || i >= n || i + n >= nextn)分支,但此时finishing为true,进入if (finishing)分支,完成sweep检查的收尾工作。

注意,最后一个完成任务的线程,不一定是领取到最左stride任务的那个线程,这个得看线程的执行顺序。

上面分析都是假设每个线程都能至少领到一次任务,所以可以说:最后一个完成任务的线程,将进行sweep检查。其实精确的说,应该是最后一个来归还许可的线程,将进行sweep检查。因为还有一种特殊情况,这种线程运气不好,进入transfer后从来没有领取到过任务,即进入while (advance)循环后每次CAS尝试修改transferIndex来领取任务都失败了,直到transferIndex变成0,然后将由这个线程来做sweep检查。

哈希桶分离的处理

哈希桶分离的原理已经讲过,我们来看具体实现吧。

链表分离处理

if (fh >= 0)分支的逻辑,就是链表分离处理。分离前桶里既有低桶元素也有高桶元素,现在称低桶与低桶同阶级,高桶与高桶同阶段,那么lastRun则是用来从尾到头找到和最后一个元素连续同阶级的第一个元素。ln hn则分别代表了低桶的lastRun(如果最后一个元素是低桶元素)、高桶的lastRun(如果最后一个元素是高桶元素)。
JUC集合类 ConcurrentHashMap源码解析 JDK8_第2张图片
现在假设执行完第一个for循环for (Node p = f.next; p != null; p = p.next)后,当前哈希桶的状态如上图第一个状态:可见,最后一个元素是高桶元素,lastRun指向了连续的也是高桶的那个元素。因为最后一个元素是高桶,所以hn也指向lastRun,但ln就只能是null了。

之后新建1节点时,新建节点和原节点的key、value都是指向同一个对象的。之后的过程,就是按照头插法新建链表的过程:新建一个低桶节点相连后,就会更新ln;新建一个高桶节点相连后,就会更新hn

从上图最后一个状态可见,hn就是新table的高桶的头节点,ln就是新table的低桶的头节点。而且新建的两个哈希桶的链表结构,完全不影响旧哈希桶。只是,分离后的两个新哈希桶里的元素,顺序有所改变,一部分或全部,都会变成倒序。

lastRun的好处相信大家也看图能理解到了,因为从lastRun以后的节点包括它自己,是不需要新建节点的,这就减少了不必要的操作。
JUC集合类 ConcurrentHashMap源码解析 JDK8_第3张图片
而上面两种特殊情况,就更加体现了lastRun的作用。这两种情况,哈希桶全是同阶级的节点,那么在逻辑里,第二个for循环for (Node p = f; p != lastRun; p = p.next)都不会去执行了。

红黑树分离处理

红黑树分离则没有使用lastRun这种机制,在for循环中,每个旧节点它都会生成相应的新节点。然后低桶高桶都会重新组织一次红黑树结构。

不能使用lastRun机制,必须生成每一个新节点。

  1. 树形结构是无法寻找lastRun的。
  2. 就算寻找到了,那就意味着要重复利用旧节点,但在new TreeBin(lo)重新组织树结构时,会打乱原哈希桶的树形结构,而这样就会影响到并发的读操作,所以,红黑树分离处理必须生成每一个新节点。
                            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;

红黑树分离处理同样处理了两种特殊情况。hc != 0lc != 0两种情况。

扩容不影响并发的读操作

从上面两种数据结构的分离操作来看,旧哈希桶的结构从来没有发生过变化。而对于读操作来说,有一个重要的时间节点,那就是处理哈希桶完毕时执行的setTabAt(tab, i, fwd)

  • 如果读操作在setTabAt(tab, i, fwd)之前,就获得i索引数组成员的引用,那么读操作就在原哈希桶里进行读操作。
  • 如果读操作在setTabAt(tab, i, fwd)之后,此时已经执行过了setTabAt(nextTab, i, ln)setTabAt(nextTab, i + n, hn),说明低桶和高桶已经放到了新table上去了。所以,读操作可以通过ForwardingNode得到新table,从而在新table里继续读操作。

扩容期间,成员的变化

  1. nextTable从null更新为二倍大小数组,transferIndex从0更新为n。
  2. transferIndex不断减小,随着任务的领取。
  3. nextTable成员不断更新,随着每个旧哈希桶分离出来的高桶低桶的赋值。
  4. sizeCtl不断减小,随着线程归还许可。
  5. 随着最后一次任务的领取,transferIndex变成0。
  6. 随着最后一个完成任务的线程归还许可,sizeCtl的线程数变成1。
  7. nextTable = nullnextTable成员置null。此时table数组的每个非null成员都是ForwardingNode。
  8. table = nextTabtable成员置为新数组。
  9. sizeCtl = (n << 1) - (n >>> 1),此时sizeCtl才置为阈值。之前的步骤中,sizeCtl都还为负数。

可见在某些步骤上,其他线程可能会发现ConcurrentHashMap处于一种中间状态上。

最后的sweep检查是没有必要的

根据Doug Lea在[concurrency-interest]的回答可知,这个sweep扫查机制是之前版本遗留下来的,本来是可以删除掉的。

删除掉没有任何影响,因为当最后一个线程来归还许可时,这意味着其他线程已经归还了许可。且只有在当前线程 完成了领取的任务后,才会归还许可,所以当最后一个线程来归还许可时,每个哈希桶都已经完成了转移,所以说最后的sweep检查是没有必要的。

get 查找操作

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());//算出哈希值,肯定是正数
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {//哈希桶的头节点与参数的哈希值相同
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))//需要判断是否equal相等
                    return e.val;
            }
            //小于0说明是一个特殊节点
            //1. 为ForwardingNode节点,说明当前在扩容中,需要转发到nextTable上寻找
            //2. 为TreeBin节点,说明哈希桶是红黑树结构,需要二叉查找
            //3. 为ReservationNode节点,说明当前槽位之前是null,这里只是占位,所以直接返回null
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //进行链表的检查
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        //没有找到,则返回null
        return null;
    }

该函数在开头并没有检查参数key是否为null,这是个隐患,不过int h = spread(key.hashCode())这里会给调用者抛出空指针异常。

返回null代表没有找到这个key。

ForwardingNode的find

    static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }

        Node<K,V> find(int h, Object k) {
            // loop to avoid arbitrarily deep recursion on forwarding nodes
            outer: for (Node<K,V>[] tab = nextTable;;) {//直接去nextTable上寻找
                Node<K,V> e; int n;
                if (k == null || tab == null || (n = tab.length) == 0 ||//如果
                    (e = tabAt(tab, (n - 1) & h)) == null)//如果哈希桶为null
                    return null;
                for (;;) {
                    int eh; K ek;
                    if ((eh = e.hash) == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                    if (eh < 0) {
                        if (e instanceof ForwardingNode) {
                            tab = ((ForwardingNode<K,V>)e).nextTable;
                            continue outer;//继续外循环,避免深递归
                        }
                        else//只可能是红黑树节点,或者ReservationNode节点
                            return e.find(h, k);
                    }
                    //如果不是特殊节点,那就是普通链表的节点,后移e
                    if ((e = e.next) == null)
                        return null;
                }
            }
        }
    }
  • 因为ForwardingNode节点持有nextTable,所以我们直接去nextTable上寻找。
  • 用双层循环,在检测到nextTable的节点又是ForwardingNode节点时,continue外层循环,以避免递归层数太高。

remove 删除操作

    public V remove(Object key) {
        return replaceNode(key, null, null);
    }

先提前说下参数:

  • key,你想要的key。
  • cv,如果找到了key,对这个映射的value的要求。
    • 如果cv为null,代表没有要求,或者肯定满足要求。即找到了满足要求的映射。
    • 如果cv不为null,必须判断cv和旧value是equal的,才能算找到了满足要求的映射。
  • value,在找到了满足要求的映射后,如果value为null,说明执行删除操作。如果value不为null,说明执行替换操作。

根据以上讲解,replaceNode(key, null, null)代表,只有能找到包含key的映射,不用管旧value,然后删除掉这个映射。

    final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode());
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)
                break;
            else if ((fh = f.hash) == MOVED)//如果扩容中,帮忙扩容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                boolean validated = false;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        //如果是链表节点
                        if (fh >= 0) {
                            validated = true;
                            for (Node<K,V> e = f, pred = null;;) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {//判断key相等
                                    V ev = e.val;
                                    //cv为null代表对这个映射的value没有要求
                                    //cv不为null,则需要比较两个value是否相等
                                    if (cv == null || cv == ev ||
                                        (ev != null && cv.equals(ev))) {
                                        oldVal = ev;
                                        if (value != null)//想替换过去的新value不为null,才能替换
                                            e.val = value;
                                        else if (pred != null)//没法替换,只好执行链表的删除操作
                                            pred.next = e.next;
                                        else//没法替换,且当前是头节点,只好新设置头节点
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;//找到key,最终都会退出循环
                                }
                                //没有找到key
                                pred = e;//保存前驱
                                if ((e = e.next) == null)//移动e
                                    break;
                            }
                        }
                        //如果是红黑树节点
                        else if (f instanceof TreeBin) {
                            validated = true;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            if ((r = t.root) != null &&
                                (p = r.findTreeNode(hash, key, null)) != null) {//直接调用红黑树的函数
                                V pv = p.val;
                                //cv为null代表对这个映射的value没有要求
                                //cv不为null,则需要比较两个value是否相等
                                if (cv == null || cv == pv ||
                                    (pv != null && cv.equals(pv))) {
                                    oldVal = pv;
                                    if (value != null)//想替换过去的新value不为null,才能替换
                                        p.val = value;
                                    else if (t.removeTreeNode(p))//没法替换,只好执行红黑树的删除操作
                                        //返回true,代表应该反树化
                                        //返回false,那就啥也不干就好
                                        setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        }
                    }
                }
                if (validated) {
                    if (oldVal != null) {//如果保存了旧值(只有找到满足要求的映射,oldVal才会保存下来)
                        if (value == null)//想替换过去的新value为null,说明之前执行的删除操作
                            addCount(-1L, -1);//第一个参数代表计数减一,第二个-1代表不用检查扩容
                        return oldVal;
                        //只要找到了key,而且对应的value符合要求,那么不管是替换操作,还是删除操作,都返回旧值
                    }
                    break;
                }
            }
        }
        return null;//相反,没有找到key,那就肯定返回null
    }

代码比较简单,代码思路和对参数的解释一致。
也有其他public函数调用到replaceNode,只是实参有所不同。

	//value为null,肯定返回false
    public boolean remove(Object key, Object value) {
        if (key == null)
            throw new NullPointerException();
        return value != null && replaceNode(key, null, value) != null;
    }
	//三个参数都不允许为null,否则空指针异常
    public boolean replace(K key, V oldValue, V newValue) {
        if (key == null || oldValue == null || newValue == null)
            throw new NullPointerException();
        return replaceNode(key, newValue, oldValue) != null;
    }
	//两个参数都不允许为null,否则空指针异常。对映射的value没有要求
    public V replace(K key, V value) {
        if (key == null || value == null)
            throw new NullPointerException();
        return replaceNode(key, value, null);
    }

size 获得大小

首先要了解到,当容量到达MAXIMUM_CAPACITY后,就不会再扩容了。比如在addCount里,需要满足(n = tab.length) < MAXIMUM_CAPACITY的条件后,才可能去扩容。此时意味着,就算大小超过了阈值,table也不会变成二倍大小了。甚至于大小会超过,int的最大值。

    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
        		//所以当发现大小已经超过int最大值,只能返回int最大值
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }

所以当发现大小已经超过int最大值,只能返回int最大值。这是一个历史遗留问题,因为这个函数的返回值被定为int了。

    public long mappingCount() {
        long n = sumCount();//clear方法可能导致小于0
        return (n < 0L) ? 0L : n; // ignore transient negative values
    }

应该尽量使用mappingCount得到一个long的大小。

clear 清空操作

    public void clear() {
        long delta = 0L; // negative number of deletions
        int i = 0;
        Node<K,V>[] tab = table;
        while (tab != null && i < tab.length) {
            int fh;
            Node<K,V> f = tabAt(tab, i);
            if (f == null)
                ++i;
            else if ((fh = f.hash) == MOVED) {
                tab = helpTransfer(tab, f);//帮忙扩容,索引从0重新开始
                i = 0; // restart
            }
            else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> p = (fh >= 0 ? f ://如果是链表节点
                                       (f instanceof TreeBin) ?//如果是红黑树节点
                                       ((TreeBin<K,V>)f).first : null);//二者都不是,那就忽略
                        while (p != null) {
                            --delta;//累计
                            p = p.next;//不管是链表还是红黑树,最后都可以当作链表
                        }
                        setTabAt(tab, i++, null);//遍历完哈希桶,置空slot
                    }
                }
            }
        }
        if (delta != 0L)
            addCount(delta, -1);//计数减去delta,第二个参数代表不用检查扩容必要性
    }

由于加锁粒度很细(对每个哈希桶加锁),可能存在并发问题,delta代表的计数可能因为并发的插入或删除,而变得“不正确”。这里是指,clear遍历过程中,别的线程并发的插入或删除了clear遍历过的哈希桶,从而导致不正确。

所以此函数可能返回负值,说明别的线程并发的删除了clear刚遍历过的哈希桶里的节点。

迭代器

ConcurrentHashMap提供了一个Traverser用作只读迭代器。设计这个迭代器的难点在于,需要考虑并发的扩容,所以需要考虑当遍历遇到ForwardingNode时,应该怎么处理。

原理很简单,当遇到ForwardingNode时,保存当前遍历信息放入一个模拟的栈中,然后在ForwardingNode.nextTable上继续遍历。当然,在nextTable上,我们只会遍历两个桶:原哈希桶对应的低桶和高桶。如果在返回原table之前,又遇到了ForwardingNode,则继续深入,重复这样的步骤。返回上一个table之前,需要遍历完当前table的两个桶,才能返回。

因为跳转到新的table上去就像函数调用一样,所以模仿函数调用,作者设计了一个模拟的栈TableStack。它是一个链栈,入栈使用头插法,出栈使用头删法。
JUC集合类 ConcurrentHashMap源码解析 JDK8_第4张图片
原理图如上,这个过程很清晰易于理解,但代码实现看起来稍微复杂。

    //进入下一个table前,用来保存当前table的遍历信息的快照
    static final class TableStack<K,V> {
        int length;//当前遍历table的大小
        int index;//当前遍历索引
        Node<K,V>[] tab;//当前遍历table
        TableStack<K,V> next;//栈的下一个元素
    }

    static class Traverser<K,V> {
        Node<K,V>[] tab;        // 当前遍历的table
        Node<K,V> next;         // 下一个将返回的Node,如果为null,需要去获取
        TableStack<K,V> stack, spare; // stack是模拟的栈,spare是保存出栈元素以备用(相当于回收站)
        int index;              // 通过index获取下一个Node
        int baseIndex;          // 最开始的table的遍历索引,在跳转table后index会变化,但baseIndex会一直保存的
        int baseLimit;          // 最开始的table的遍历索引限制
        final int baseSize;     // 最开始的table的大小

        Traverser(Node<K,V>[] tab, int size, int index, int limit) {
            this.tab = tab;
            this.baseSize = size;
            this.baseIndex = this.index = index;//如果没有发生跳转table,那么这二者总是保持一致的
            this.baseLimit = limit;
            this.next = null;
        }

        /**
         * 获得下一个节点,如果到头了,就返回null
         */
        final Node<K,V> advance() {
            Node<K,V> e;
            if ((e = next) != null)//首先从next获得,一般情况下next已经准备好
                e = e.next;
            for (;;) {
                Node<K,V>[] t; int i, n;
                if (e != null)
                    return next = e;//准备好next
                //如果next没有准备好,则需要通过index获得了

                //如果遍历到头了
                if (baseIndex >= baseLimit || (t = tab) == null ||//超过了限制
                    (n = t.length) <= (i = index) || i < 0)//索引到达size
                    return next = null;
                //如果是特殊节点
                if ((e = tabAt(t, i)) != null && e.hash < 0) {
                    if (e instanceof ForwardingNode) {//需要转发table了
                        tab = ((ForwardingNode<K,V>)e).nextTable;//更新tab为新table
                        e = null;//下一个循环需要重新获取
                        pushState(t, i, n);//入栈,保存旧table的当前遍历信息
                        continue;//下一次循环,直接获得新table的低桶的头节点
                    }
                    else if (e instanceof TreeBin)//如果是红黑树
                        e = ((TreeBin<K,V>)e).first;//当作单链表使用
                    else//保留节点,没有数据
                        e = null;//下一个循环需要重新获取
                }
                if (stack != null)//如果栈不为空
                    //1. 要么还在当前新table,但索引移动到高桶索引
                    //2. 要么回到旧table,索引也根据快照恢复
                    recoverState(n);
                //如果栈为空
                //如果n为最开始的table的大小,那么无论i是什么,条件肯定成立
                //如果n已经是新table的大小了,那么无论i是什么,条件不成立,让i变成高桶索引
                //(这其实不可能,实际过程中,索引变成高桶索引的任务,都交给了recoverState)
                else if ((index = i + baseSize) >= n)
                    //如果是最开始的table,需要恢复索引为正常索引+1
                    index = ++baseIndex; // visit upper slots if present
            }
        }

        /**
         * 入栈操作,保存当前遍历信息
         */
        private void pushState(Node<K,V>[] t, int i, int n) {
            TableStack<K,V> s = spare;  // 如果spare不为null
            if (s != null)
                spare = s.next;
            else
                s = new TableStack<K,V>();
            s.tab = t;
            s.length = n;
            s.index = i;
            //这两句以头插法,入栈
            s.next = stack;
            stack = s;//让栈指针指向栈顶
        }

        /**
         * 1. 将索引成员移动到高桶索引
         * 2. 高桶已经遍历完,此函数将根据栈顶元素回到上一个table,
         *    如果发现上一个table也遍历完了(恢复快照后发现索引处于高桶索引),
         *    将回到上上个table
         */
        private void recoverState(int n) {//n为当前遍历的table的大小
            TableStack<K,V> s; int len;
            //第二个条件不成立,此函数执行的是1过程
            //第二个条件成立,此函数执行的是2过程。如果发现上一个table也遍历完了,循环将再次进入
            while ((s = stack) != null && (index += (len = s.length)) >= n) {
                //将栈顶元素的信息赋值给成员
                n = len;//局部变量n恢复成更小table的大小
                index = s.index;
                tab = s.tab;
                s.tab = null;
                TableStack<K,V> next = s.next;
                s.next = spare; // 将清空的s头插法放到spare里备用
                stack = next;//栈顶指针下移,相当于出栈
                spare = s;//s现在是spare的头指针
            }
            //如果stack为null,说明当前遍历已经回到了最开始的table,需要根据baseSize恢复index
            if (s == null && (index += baseSize) >= n)
                index = ++baseIndex;
        }
    }

过程分析

直接看代码不好理解,所以假设遍历过程如下图。根据这个过程,我们来看Traverser的执行过程。

假设所有哈希桶都是链表结构,方便分析。其实就算是红黑树也无所谓,因为也是当作链表来使用的。
JUC集合类 ConcurrentHashMap源码解析 JDK8_第5张图片
①步骤:

  • 刚遍历完这个哈希桶,next为null,索引成员(指index)为图中的index。此时有人执行了advance,进入死循环。不会进入if (e != null) return next = e;,然后通过index获得下一个遍历索引的头节点,发现是个ForwardingNode。
  • 迭代器tab成员更新为2倍table,执行pushState后,stack保存了1倍table的执行信息快照(栈元素有一个),spare为null。continue下一次循环。

②步骤:

  • 执行recoverState(n),注意此时传入的n是2倍table(的大小,后面将忽略这半句话)。执行index += (len = s.length)) >= n,此时s为栈顶元素的table的大小,即len为1倍table,n为2倍table,所以这个条件肯定不成立(因为index是一个小于1倍table大小),此时index变成高桶索引。后面的if (s == null && (index += baseSize) >= n)由于短路,也不进入分支。
  • 也就是说,这次recoverState(n)负责将索引变成高桶索引 index+1倍table。在③步骤之前,循环将遍历完2倍table的低桶。

③步骤:

  • 刚遍历完这个哈希桶,next为null,索引为 index+1倍table。发现是个ForwardingNode。
  • 迭代器tab成员更新为4倍table,执行pushState后,stack保存了2倍table的执行信息快照(栈元素有两个),spare为null。continue下一次循环。

④步骤:

  • 执行recoverState(n),注意此时传入的n是4倍table。执行index += (len = s.length)) >= n,此时s为栈顶元素的2倍table的大小,即len为2倍table,n为4倍table,注意此时索引为 index+1倍table,再加2倍table,也不可能大于4倍table大小。所以这个条件肯定不成立,此时index变成高桶索引。后面的if (s == null && (index += baseSize) >= n)由于短路,也不进入分支。
  • 也就是说,这次recoverState(n)负责将索引变成高桶索引 index+3倍table。在⑤步骤之前,循环将遍历完4倍table的低桶。

⑤⑥⑦步骤(相当于一次完成了):

  • 刚遍历完这个哈希桶,next为null,索引为 index+3倍table(可以看成两部分,index+1倍table 和 2倍table)。发现是个链表节点。
  • 进入if (stack != null) recoverState(n);,注意此时传入的n是4倍table。执行index += (len = s.length)) >= n,此时s为栈顶元素的2倍table的大小,即len为2倍table,n为4倍table,加完后索引为 index+5倍table,条件成立,进入循环。
    • 局部变量n变成2倍table。
    • 索引恢复为2倍table的快照的那个索引,也就是index+1倍table。
    • tab成员恢复为2倍table。
    • 将栈顶元素的信息赋值给成员,从stack的栈顶元素出栈,出栈前被清空。但为了不浪费创建出的对象,又把出栈元素又放到spare里,下一次如果发现spare里面有元素,就不用创建对象了。
    • 此时stack只有一个元素,1倍table的快照。spare有一个空闲对象。
  • 下一次while循环,index += (len = s.length)) >= n依旧成立,进入循环。(因为此时,2倍table的遍历也已经完成了,需要继续处理)
    • 局部变量n变成1倍table。
    • 索引恢复为1倍table的快照的那个索引,也就是index。
    • tab成员恢复为1倍table。
    • stack被清空,spare有两个空闲对象。
  • 下一次while循环,不会进入,因为stack为null。
  • 后面的if (s == null && (index += baseSize) >= n)进入,因为此时n为1倍table,baseSize也为1倍table。此时baseIndex保存着1倍table的索引的快照,所以用它来恢复index(index = ++baseIndex)。
  • 之后循环将遍历完4倍table的高桶。

视图

主要讲一下KeySet吧,因为JUC框架中并没有一个类叫做ConcurrentHashSet,我们可以通过Set s = ConcurrentHashMap. newKeySet()的方式得到一个线程安全的set。它的实现其实就是依赖于一个ConcurrentHashMap,这里只要知道它是继承了Set接口即可。

    public static <K> KeySetView<K,Boolean> newKeySet() {
        return new KeySetView<K,Boolean>
            (new ConcurrentHashMap<K,Boolean>(), Boolean.TRUE);
    }

    public static class KeySetView<K,V> extends CollectionView<K,V,K>
        implements Set<K>, java.io.Serializable {
    	...
    }

ReservationNode

三种特殊节点,红黑树节点和转发节点ForwardingNode我们都讲过了。ReservationNode用作一个占位节点,在computeIfAbsent和compute函数使用中。

computeIfAbsent

	//1. 当map中没有包含参数key的映射,该函数才添加映射。
	//2. 添加失败时,返回已存在映射的旧value;添加成功时,返回null。
	//3. 添加的映射的key已经给出,但value需要通过Function算出来。
    public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
        if (key == null || mappingFunction == null)
            throw new NullPointerException();
        int h = spread(key.hashCode());//根据原始hash值,获得处理后的hash值
        V val = null;
        int binCount = 0;//将代表一个哈希桶内节点数
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //如果发现table还没初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果发现索引所在元素为null,那么就CAS该null元素
            else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
                Node<K,V> r = new ReservationNode<K,V>();
                synchronized (r) {
                    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);
                            //不管Function执行结果如何,都把这个执行结果赋值过去。node可能是null
                        }
                    }
                }
                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;//如果找到了包含key的映射,那么不执行函数,返回旧value
                                    //因为这属于添加失败
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {//遍历到最后,都没有找到包含key的映射
                                    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 &&
                                //如果找到了包含key的映射,那么不执行函数,返回旧value
                                (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;//添加失败时,val不为null;添加成功时,val必为null
    }

该函数的实现和putVal很像啊,因为这个函数做的其实也是插入操作嘛,自然会很像。

  • 当map中没有包含参数key的映射,该函数才添加映射。
  • 添加失败时,返回已存在映射的旧value;添加成功时,返回null。
  • 添加的映射的key已经给出,但value需要通过Function算出来。

compute

分析类似,看注释即可。

    public V compute(K key,
                     BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        if (key == null || remappingFunction == null)
            throw new NullPointerException();
        int h = spread(key.hashCode());
        V val = null;
        int delta = 0;
        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) {
                    if (casTabAt(tab, i, null, r)) {
                        binCount = 1;
                        Node<K,V> node = null;
                        try {
                            if ((val = remappingFunction.apply(key, null)) != null) {
                                delta = 1;
                                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 {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f, pred = null;; ++binCount) {
                                K ek;
                                if (e.hash == h &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {//如果找到了包括key的映射
                                    val = remappingFunction.apply(key, e.val);
                                    //如果算出来的value不为null
                                    if (val != null)
                                        e.val = val;//替换操作
                                    //如果算出来的value为null
                                    else {
                                        delta = -1;
                                        Node<K,V> en = e.next;
                                        //删除操作
                                        if (pred != null)
                                            pred.next = en;
                                        else
                                            setTabAt(tab, i, en);
                                    }
                                    break;
                                }
                                pred = e;
                                if ((e = e.next) == null) {//如果没有找到包含key的映射
                                    val = remappingFunction.apply(key, null);//计算新value
                                    if (val != null) {
                                        delta = 1;
                                        pred.next =
                                            new Node<K,V>(h, key, val, null);//新增操作
                                    }
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            binCount = 1;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            if ((r = t.root) != null)
                                p = r.findTreeNode(h, key, null);
                            else
                                p = null;
                            V pv = (p == null) ? null : p.val;//如果找到了映射,保存旧值
                            val = remappingFunction.apply(key, pv);//根据旧值计算出新值
                            //新值不为null
                            if (val != null) {
                                if (p != null)//如果映射被找到
                                    p.val = val;//替换操作
                                else {//如果映射没被找到
                                    delta = 1;
                                    t.putTreeVal(h, key, val);//新增操作
                                }
                            }
                            //新值为null,而且映射之前被找到,执行
                            else if (p != null) {
                                delta = -1;
                                //删除操作
                                if (t.removeTreeNode(p))
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    break;
                }
            }
        }
        if (delta != 0)
            addCount((long)delta, binCount);
        return val;
    }

第一步,计算出新值val

  • 如果包含参数key的映射被找到,那么计算公式为remappingFunction.apply(key, 旧value)
  • 如果包含参数key的映射没被找到,那么计算公式为remappingFunction.apply(key, null)
  • 不管怎样,新值val都会被计算出来,并且作为返回值返回。

第二步,决定要执行哪种操作。但首先得知道,ConcurrentHashMap是不允许value为null的,但是新值val计算出来却可以为null。

  • 如果包含参数key的映射被找到。
    • 如果新值val计算出来不为null,那么执行替换操作
    • 如果新值val计算出来为null,那么执行删除操作
  • 如果包含参数key的映射没被找到。
    • 如果新值val计算出来不为null,那么执行新增操作
    • 如果新值val计算出来为null,那么啥也不干

ReservationNode节点存在的必要性

占位以后,别的插入操作就无法插入了,因为插入操作还没法获得到锁。

Unsafe的使用

    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

    private static final long ABASE;
    private static final int ASHIFT;

    static {
        try {
            Class<?> ak = Node[].class;
            ABASE = U.arrayBaseOffset(ak);
            int scale = U.arrayIndexScale(ak);
            ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
        } catch (Exception e) {
            throw new Error(e);
        }
    }
  • ABASE简单理解为,第一个数组元素开始存储的偏移地址。(猜想:一个数组最开始存的可能是元素类型Class、或者数组长度,之后才开始存储第一个数组元素)
  • 数组元素的大小为 2 A S H I F T 2^{ASHIFT} 2ASHIFT。由于Node是引用类型,scale肯定为4即100,它的前面还有29个0,所以ASHIFT为2。其实ASHIFT就是100后面的0的个数,而一个数左移ASHIFT位,就相当于一个数乘以 2 A S H I F T 2^{ASHIFT} 2ASHIFT
  • 上一条解释了,tabAt之类的方法里,使用了(long)i << ASHIFT来代替(long)i * 2^ASHIFT

总结

  • ConcurrentHashMap的基础操作还是和HashMap一样的,比如table取下标使用& (n-1)而不是% n,哈希值需要高位扰动,哈希桶分离分为低桶和高桶。
  • 对于数据结构为红黑树的节点,TreeBin封装了真正的红黑色节点TreeNodeTreeBin起到一个dummy node的作用,避免了红黑树平衡造成的table槽位成员发生变化。
  • ConcurrentHashMap不允许key或者value为null
  • 对于所有的写操作(插入操作、插入后的树化操作、哈希桶分离操作…),在写操作之前,都需要对哈希桶的第一个节点加锁。这是有效的,因为在链表中,节点总是添加到末尾,头节点不会变,直到它被删除或哈希桶分离。红黑树的TreeBin也是总是不变的,除非哈希桶分离。
  • 计数器的任务,交给了baseCountcounterCells,它们的实现类似于LongAdder,这比使用AtomicLong用作计数好多了。因为很大程度减小了因CAS失败而导致的自旋。
  • transfer是扩容的真正实现,这一方法被设计成可以被多个线程并发执行,以加快扩容的完成。使用ForwardingNode来连接旧table的节点和新table。
  • 提供了一个Traverser用作只读迭代器,它在遇到ForwardingNode时进行table跳转,然后在新table上读取相应的低桶和高桶。
  • 写操作的加锁粒度是每个哈希桶。
    • 好处是读写操作可以最大程序并发执行,这样效率最高。
    • 坏处是读写操作都是弱一致性,比如size()返回的大小可能已经与真实大小不一样,比如clear()调用返回后Map中却拥有着元素。

你可能感兴趣的:(Java)