深入并发-ConcurrentHashMap

概述

关于Java集合的小抄是这么描述:

  • 并发优化的HashMap。

  • 在JDK5里的经典设计,默认16把写锁(可以设置更多),有效分散了阻塞的概率。数据结构为Segment[],每个Segment一把锁。Segment里面才是哈希桶数组。Key先算出它在哪个Segment里,再去算它在哪个哈希桶里。

  • 也没有读锁,因为put/remove动作是个原子动作(比如put的整个过程是一个对数组元素/Entry 指针的赋值操作),读操作不会看到一个更新动作的中间状态。

  • 但在JDK8里,Segment[]的设计被抛弃了,改为精心设计的,只在需要锁的时候加锁。

  • 支持ConcurrentMap接口,如putIfAbsent(key,value)与相反的replace(key,value)与以及实现CAS的replace(key, oldValue, newValue)。

源码分析

成员变量

  • table:默认为null,初始化发生在第一次插入操作,默认大小为16的数组,用来存储Node节点数据,扩容时大小总是2的幂次方。

  • nextTable:默认为null,扩容时新生成的数组,其大小为原数组的两倍。

  • sizeCtl :默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。

等于-1时,代表 table 正在初始化 
等于-N时,表示有N-1个线程正在进行扩容操作 
如果table未初始化,表示table需要初始化的大小。 
如果table初始化完成,表示table的容量,默认是table大小的0.75倍,居然用这个公式算0.75(n - (n >>> 2))。
  • Node:保存key,value及key的hash值的数据结构。 其中value和next都用volatile修饰,保证并发的可见性。
class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    //... 省略部分代码
}
  • ForwardingNode:一个特殊的Node节点,hash值为-1,其中存储nextTable的引用。 只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动。
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;
    }
}

初始化数组

初始化数组的时候需要判断是否有其他线程正在执行初始化,采用CAS操作更新 sizeCtl 的值。具体代码如下:

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        //如果一个线程发现sizeCtl<0,意味着另外的线程执行CAS操作成功正在初始化表,当前线程只需要让出cpu时间片
        if ((sc = sizeCtl) < 0)
            Thread.yield();
        //通过 cas 操作,将 sizeCtl 替换为-1,标识当前线程抢占到了初始化资格
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    //没有指定初始化容量大小,则默认为16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    //计算下次扩容的大小,实际就是当前容量的 0.75 倍,这里使用了右移来计算
                    sc = n - (n >>> 2);
                }
            } finally {
                //设置sizeCtl为sc, 如果默认是16的话,那么这个时候 sc=16*0.75=12
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

put函数

与 HashMap 的 put 操作类似,主要增加多线程情况的判断。具体实现如下:

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

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    //这里其实就是自旋操作,当出现线程竞争时不断自旋
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //如果tab为空则初始化table
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //通过 hash 值对应的数组下标得到第一个节点; 以 volatile 读的方式来读取 table 数组中的元素,保证每次拿到的数据都是最新的
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //如果该下标返回的节点为空,则直接通过 cas 将新的值封装成 node 插入即可;如果 cas 失败,说明存在竞争,则进入下一次循环
            if (casTabAt(tab, i, null,
                    new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 当前hash为MOVED表示Map在扩容,先协助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            //hash冲突,通过 synchronized 加锁当前位置对象防止多线程更新
            V oldVal = null;
            synchronized (f) {
                //再次判断是否已被其他线程更新值
                if (tabAt(tab, i) == f) {
                    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;
                            }
                        }
                    }
                    //链表节点 >= 8个则已经转换为红黑树,遍历寻找进行替换值或新增节点
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                value)) != null) {
                            //同样,如果值已经存在,则直接替换
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                //如果链表节点个数 >= 8,则转换为红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //将当前 ConcurrentHashMap 的元素数量加 1,有可能触发 transfer 操作(扩容)
    addCount(1L, binCount);
    return null;
}

tabAt

该方法获取对象中 offset 偏移地址对应的对象 field 的值。实际上这段代码的含义等价于 tab[i], 但是为什么不直接使用 tab[i]来计算呢?
getObjectVolatile,一旦看到 volatile 关键字,就表示可见性。因为对 volatile 写操作 happen- before 于 volatile 读操作,因此其他线程对 table 的修改均对 get 读取可见;虽然 table 数组本身是增加了 volatile 属性,但是“volatile 的数组只针对数组的引用具有 volatile 的语义,而不是它的元素”。所以如果有其他线程对这个数组的元素进行写操作,那么当前线程来读的时候不一定能读到最新的值。
出于性能考虑,Doug Lea 直接通过 Unsafe 类来对 table 进行操作。

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);
}

size

为了更好地统计size,ConcurrentHashMap提供了baseCount、counterCells两个辅助变量和一个CounterCell辅助内部类。
ConcurrentHashMap 是并发集合,如果用一个成员变量来统计元素个数的话,为了保证并发情况下共享变量的的及时更新,势必会需要通过加锁或者自旋 CAS 来实现,如果竞争比较激烈的情况下,size 的设置上会出现比较大的冲突反而影响了性能,所以在 ConcurrentHashMap 采用了分片的方法来记录大小。

	// 标识当前 cell 数组是否在初始化或扩容中的 CAS 标志位
  	private transient volatile int cellsBusy;
  	//counterCells数组,总数值的分值分别存在每个 cell 中
    private transient volatile CounterCell[] counterCells;
    
    @sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }

    //ConcurrentHashMap中元素个数,但返回的不一定是当前Map的真实元素个数。基于CAS无锁更新
    private transient volatile long baseCount;

CounterCell 数组的每个元素,都存储一个元素个数,而实际我们调用 size 方法就是通过这个循环累加来得到的。

    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                        (int)n);
    }

    //迭代counterCells来统计sum的过程
    final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;//所有counter的值求和
            }
        }
        return sum;
    }

数组扩容

当ConcurrentHashMap中table元素个数达到了容量阈值(sizeCtl)时,则需要进行扩容操作。在put操作时最后一个会调用addCount(long x, int check),该方法主要做两个工作:
1.更新 baseCount;
2.检测是否需要扩容操作。

fullAddCount

我们首先来看下是如何通过 counterCells 记录数组大小

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        // 判断 counterCells 是否为空,
        //如果为空,就通过 cas 操作尝试修改 baseCount 变量,对这个变量进行原子累加操作(做这个操作的意义是:如果在没有竞争的情况下,仍然采用 baseCount 来记录元素个数)
        //如果 cas 失败说明存在竞争,这个时候不能再采用 baseCount 来累加,而是通过 CounterCell 来记录
        if ((as = counterCells) != null ||
                !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            /**
             * 这里有几个判断
             * 1. 计数表为空则直接调用 fullAddCount
             * 2. 从计数表中随机取出一个数组的位置为空,直接调用 fullAddCount
             * 3. 通过 CAS 修改 CounterCell 随机位置的值,如果修改失败说明出现并发情况(这里又用到了一种巧妙的方法),调用 fullAndCount
             * Random 在线程并发的时候会有性能问题以及可能会产生相同的随机 数,ThreadLocalRandom.getProbe 可以解决这个问题,并且性能要比 Random 高
             */
            if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                    !(uncontended =
                            U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                //  多线程CAS发生失败时执行
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
    }

fullAddCount 主要是用来初始化 CounterCell,来记录元素个数,里面包含扩容,初始化等操作

private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        //获取当前线程的 probe 的值,如果值为 0,则初始化当前线程的 probe 的值,probe 就是随机数
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit(); // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;// 由于重新生成了probe,未冲突标志位设置为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) {
                if ((a = as[(n - 1) & h]) == null) {// 通过该值与当前线程 probe 求与,获得 cells 的下标元素,和 hash 表获取索引是一样的
                    if (cellsBusy == 0) { //cellsBusy=0 表示 counterCells 不在初始化或者扩容状态下
                        CounterCell r = new CounterCell(x); //构造一个CounterCell的值,传入元素个数
                        if (cellsBusy == 0 &&//通过cas设置cellsBusy标识,防止其他线程来 对 counterCells 并发处理
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            boolean created = false;
                            try { // Recheck under lock
                                CounterCell[] rs;
                                int m, j;
                                //将初始化的 r 对象的元素个数放在对应下标的位置
                                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;//说明指定 cells 下标位置的数据不为空,则进行下一次循环
                        }
                    }
                    collide = false;
                }
                //说明在 addCount 方法中 cas 失败了,并且获取 probe 的值不为空
                else if (!wasUncontended)
                    wasUncontended = true; //设置为未冲突标识,进入下一次自旋
                //由于指定下标位置的 cell 值不为空,则直接通过 cas 进行原子累加,如果成功,则直接退出
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                //如果已经有其他线程建立了新的 counterCells 或者 CounterCells 大于 CPU 核心数 (很巧妙,线程的并发数不会超过 cpu 核心数)
                else if (counterCells != as || n >= NCPU)
                    collide = false; //设置当前线程的循环失败不进行扩容
                else if (!collide)//恢复collide状态,标识下次循环会进行扩容
                    collide = true;
                //进入这个步骤,说明 CounterCell 数组容量不够,线程竞争较大,所以先设置一个标识表示为正在扩容
                else if (cellsBusy == 0 &&
                        U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {// Expand table unless stale
                            //扩容一倍 2 变成 4,这个扩容比较简单
                            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;//继续下一次自旋
                }
                h = ThreadLocalRandom.advanceProbe(h);//更新随机数的值
            }
            //cellsBusy=0 表示没有在做初始化,通过 cas 更新 cellsbusy 的值标注当前线程正在做初始化操作
            else if (cellsBusy == 0 && counterCells == as &&
                        U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    boolean init = false;
                    try { // Initialize table
                        if (counterCells == as) {
                            CounterCell[] rs = new CounterCell[2]; //初始化容量为 2
                            rs[h & 1] = new CounterCell(x);//将 x 也就是元素的个数放在指定的数组
                            下标位置
                                counterCells = rs;//赋值给counterCells
                            init = true;//设置初始化完成标识
                        }
                    } finally {
                        cellsBusy = 0;//恢复标识
                    }
                    if (init)
                        break;
            }
            //竞争激烈,其它线程占据 cell 数组,直接累加在 base 变量中
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;
        }
    }
transfer

其次我们来看下关于数组扩容的代码

		/**
         * 判断是否需要扩容,也就是当更新后的键值对总数 baseCount >= 阈值 sizeCtl 时,进行rehash,这里面会有两个逻辑。
         * 1. 如果当前正在处于扩容阶段,则当前线程会加入并且协助扩容
         * 2. 如果当前没有在扩容,则直接触发扩容操作
         */
        if (check >= 0) {//如果 binCount>=0,标识需要检查扩容
            Node<K,V>[] tab, nt; int n, sc;
            //s 标识集合大小,如果集合大小大于或等于扩容阈值(默认值的 0.75)并且 table 不为空并且 table 的长度小于最大容量则进行扩容
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                    (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);//这里是生成一个唯一的扩容戳
                //sc<0,也就是sizeCtl<0,说明已经有别的线程正在扩容了
                if (sc < 0) {
                    /**
		             * 这 5 个条件只要有一个条件为 true,说明当前线程不能帮助进行此次的扩容,直接跳出循环
		             * 1. sc >>> RESIZE_STAMP_SHIFT!=rs 表示比较高 RESIZE_STAMP_BITS 位生成戳和 rs 是否相等,判断是否同一次扩容
		             * 2. sc=rs+1 表示扩容结束
		             * 3. sc==rs+MAX_RESIZERS 表示帮助线程线程已经达到最大值了
		             * 4. nt=nextTable -> 表示扩容已经结束
		             * 5. transferIndex<=0 表示所有的 transfer 任务都被领取完了,没有剩余的 hash 桶来给自己自己好这个线程来做 transfer
		             */
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0)
                        break;
                    //当前线程尝试帮助此次扩容,如果成功,则调用 transfer
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                // 如果当前没有在扩容,那么 rs 肯定是一个正数,通过 rs<<RESIZE_STAMP_SHIFT 将 sc 设置 为一个负数,+2 表示有一个线程在执行扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                        (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();// 重新计数,判断是否需要开启下一轮扩容
            }
        }

resizeStamp 用来生成一个和扩容有关的扩容戳,具体有什么作用呢?我们基于它的实现来 做一个分析

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

Integer.numberOfLeadingZeros 这个方法是返回无符号整数 n 最高位非 0 位前面的 0 的个 数
比如 10 的二进制是 0000 0000 0000 0000 0000 0000 0000 1010 那么这个方法返回的值就是 28
根据 resizeStamp 的运算逻辑,我们来推演一下,假如 n=16,那么 resizeStamp(16)=32796
转化为二进制是
[0000 0000 0000 0000 1000 0000 0001 1100]
接着再来看,当第一个线程尝试进行扩容的时候,会执行下面这段代码
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
rs 左移 16 位,相当于原本的二进制低位变成了高位
[1000 0000 0001 1100 0000 0000 0000 0000]
然后+2 = [1000 0000 0001 1100 0000 0000 0000 0000]+10
=[1000 0000 0001 1100 0000 0000 0000 0010]
高 16 位代表扩容的标记、低 16 位代表并行扩容的线程数

高 RESIZE_STAMP_BITS 位低 RESIZE_STAMP_SHIFT 位
扩容标记并行扩容线程数

这样来存储有什么好处呢?

  1. 首先在 CHM 中是支持并发扩容的,也就是说如果当前的数组需要进行扩容操作,可以
    由多个线程来共同负责
  2. 可以保证每次扩容都生成唯一的生成戳,每次新的扩容,都有一个不同的 n,这个生成
    戳就是根据 n 来计算出来的一个数字,n 不同,这个数字也不同

第一个线程尝试扩容的时候,为什么是+2?

  • 因为 1 表示初始化,2 表示一个线程在执行扩容,而且对 sizeCtl 的操作都是基于位运算的, 所以不会关心它本身的数值是多少,只关心它在二进制上的数值,而 sc + 1 会在低 16 位上加 1。

transfer()方法它把 Node 数组当作多个线程之间共享的任务队列,然后通过维护一个指针来划 分每个线程锁负责的区间,每个线程通过区间逆向遍历来实现扩容,一个已经迁移完的 bucket 会被替换为一个 ForwardingNode 节点,标记当前 bucket 已经被其他线程迁移完了。

  1. fwd:这个类是个标识类,用于指向新表用的,其他线程遇到这个类会主动跳过这个类,因为这个类要么就是扩容迁移正在进行,要么就是已经完成扩容迁移,也就是这个类要保证线程安全,再进行操作。
  2. advance:这个变量是用于提示代码是否进行推进处理,也就是当前桶处理完,处理下一个 桶的标识
  3. finishing:这个变量用于提示扩容是否结束用的
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //将 (n>>>3 相当于 n/8) 然后除以 CPU 核心数。如果得到的结果小于 16,那么就使用 16 
        // 这里的目的是让每个 CPU 处理的桶一样多,避免出现转移任务不均匀的现象,如果桶较少的话,默认一个 CPU(一个线程)处理 16 个桶,也就是长度为 16 的时候,扩容的时候只会有一 个线程来扩容
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                //构建一个nextTable对象,其容量为原来容量的两倍
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;//更新转移下标,表示转移时的下标
        }
        int nextn = nextTab.length;
        //创建一个 fwd 节点,表示一个正在被迁移的 Node,并且它的 hash 值为-1(MOVED),也就是前面我们在讲 putval 方法的时候,会有一个判断 MOVED 的逻辑。
        //它的作用是用来占位,表示原数组中位置 i 处的节点完成迁移以后,就会在 i 位置设置一个 fwd 来告诉其他线程这个位置已经处理过了
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        // 首次推进为 true,如果等于 true,说明需要再次推进一个下标(i--),反之,如果是 false,那么就不能推进下标,需要将当前的下标处理完毕才能继续推进
        boolean advance = true;
        //判断是否已经扩容完成,完成就 return,退出循环
        boolean finishing = false;
        //通过 for 自循环处理每个槽位中的链表元素,默认 advace 为真,通过 CAS 设置 transferIndex 属性值,并初始化 i 和 bound 值,i 指当前处理的槽位序号,bound 指需要处理 的槽位边界;
        for (int i = 0, bound = 0;;) {
        	// 这个循环使用 CAS 不断尝试为当前线程分配任务,直到分配成功或任务队列已经被全部分配完毕
 			// 如果当前线程已经被分配过 bucket 区域,那么会通过--i 指向下一个待处理 bucket 然后退出该循环
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                //--i 表示下一个待处理的 bucket,如果它>=bound,表示当前线程已经分配过 bucket 区域
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {//表示所有bucket已经被分配完毕
                    i = -1;
                    advance = false;
                }
                //通过 cas 来修改 TRANSFERINDEX,为当前线程分配任务,处理的节点区间为 (nextBound,nextIndex)->(0,15)
                else if (U.compareAndSwapInt
                        (this, TRANSFERINDEX, nextIndex,
                                nextBound = (nextIndex > stride ?
                                        nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            //i<0 说明已经遍历完旧的数组,也就是当前线程已经处理完所有负责的 bucket
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                // 已经完成所有节点复制了
                if (finishing) {
                    nextTable = null;
                    table = nextTab;        // table 指向nextTable
                    sizeCtl = (n << 1) - (n >>> 1);     //扩容阈值设置现在容量的0.75倍
                    return;
                }
                // 每增加一个线程参与迁移就会将 sizeCtl 加 1,这里使用 CAS 操作对 sizeCtl 的低 16 位进行减 1,代表做完了属于自己的任务
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                	//如果他们相等了,说明没有线程在帮助他们扩容了。扩容结束了。
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            // 遍历的节点为null,则放入到ForwardingNode 指针节点
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            //表示该位置已经完成了迁移,也就是如果线程 A 已经处理过这个节点,那么线程 B 处理这个节点时,hash 值一定为 MOVED
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                // 节点加锁,避免多线程复制同一个节点
                synchronized (f) {
                    // 节点复制工作
                    if (tabAt(tab, i) == f) {
                    	//ln 表示低位, hn 表示高位;接下来这段代码的作用 是把链表拆分成两部分,0 在低位,1 在高位
                        Node<K,V> ln, hn;
                        // fh >= 0 ,表示为链表节点
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {//如果最后更新的runBit是0,设置低位节点
                                ln = lastRun;
                                hn = null;
                            }
                            else {否则,设置高位节点
                                hn = lastRun;
                                ln = null;
                            }
                            //构造高位以及低位的链表
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            // 在nextTable i 位置处插上链表
                            setTabAt(nextTab, i, ln);
                            // 在nextTable i + n 位置处插上链表
                            setTabAt(nextTab, i + n, hn);
                            // 在table i 位置处插上ForwardingNode 表示该节点已经处理过了
                            setTabAt(tab, i, fwd);
                            // advance = true 可以执行--i动作,遍历节点
                            advance = true;
                        }
                        // 如果是TreeBin,则按照红黑树进行处理,处理逻辑与上面一致
                        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;
                                }
                            }

                            // 扩容后树节点个数若<=6,将树转链表
                            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;
                        }
                    }
                }
            }
        }
    }

上面的源码有点儿长,稍微复杂了一些,在这里我们抛弃它多线程环境,我们从单线程角度来看:

  1. 为每个内核分任务,并保证其不小于16
  2. 检查nextTable是否为null,如果是则初始化nextTable,使其容量为table的两倍
  3. 循环变量直到 finished,利用 tabAt 方法获得 i 位置的元素(支持多线程复制)
    • 如果这个位置为空,就在原table中的 i 位置放入 ForwardingNode 节点,这个也是触发并发扩容的关键点;
    • 如果这个位置的 hash 值为 MOVED,表示该位置已经完成了迁移;
    • 如果这个位置是Node节点(fh>=0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在 nextTable 的 i 和 i+n 的位置上。并将 ForwardingNode 插入原节点位置,代表已经处理过了
    • 如果这个位置是 TreeBin 节点(fh<0),也做一个反序处理,并且判断是否需要 unTreeify() 操作,把处理的结果分别放在 nextTable 的 i 和 i+n 的位置上。并插入ForwardingNode 节点
  4. 遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新 sizeCtl 为新容量的0.75倍 ,完成扩容。

在多线程环境下,ConcurrentHashMap用两点来保证正确性:ForwardingNode和synchronized。

  • 当一个线程遍历到的节点如果是ForwardingNode,则继续往后遍历。
  • 如果不是则将该节点加锁,防止其他线程进入,完成后设置ForwardingNode节点。

当其他线程处理该节点时可以看到已经处理过了,如此交叉进行,高效而又安全。

helpTransfer

在添加、删除等方法里面都会调用,当前优先协助扩容。
helpTransfer()方法为协助扩容方法,当调用该方法的时候,nextTable一定已经创建了,所以该方法主要则是进行复制工作。

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        // 判断此时是否仍然在执行扩容,nextTab=null 的时候说明扩容已经结束了
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            int rs = resizeStamp(tab.length);
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {//说明扩容还未完成的情况下不断循环来尝试将当前 线程加入到扩容操作中
                /**
		         * 下面部分的整个代码表示扩容结束,直接退出循环
		         * sc >>> RESIZE_STAMP_SHIFT !=rs, 如果在同一轮扩容中,那么 sc 无符号右移比较高位和 rs 的值,那么应该是相等的。如果不相等,说明扩容结束了
		         * sc==rs+1 表示扩容结束
		         * sc=rs+MAX_RESIZERS 表示扩容线程数达到最大扩容线程数
		         * transferIndex<=0 表示所有的 Node 都已经分配了线程
		         */
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                //在低16位 上增加扩容线程数
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);//帮助扩容
                    break;
                }
            }
            return nextTab;
        }
        //返回新的数组
        return table;
    }

转换红黑树

用于将过长的链表转换为TreeBin对象。但是他并不是直接转换,而是进行一次容量判断。

  • 如果容量没有达到转换的要求(table.length<64),直接进行扩容操作并返回;

  • 如果满足条件才链表的结构抓换为TreeBin ,这与HashMap不同的是:

    1.根据table中index位置Node链表,重新生成一个hd为头结点的TreeNode
    2.根据hd头结点,生成TreeBin树结构,并用TreeBin替换掉原来的Node对象。

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)//tab 的长度是不是小于 64, 如果是则优先执行扩容
                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;
                        //构造了一个TreeBin对象 把所有Node节点包装成TreeNode放进去
                        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);//这里只是利用了TreeNode封装 而没有利用TreeNode的next域和parent域
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        //在原来index的位置 用TreeBin替换掉原来的Node对象
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

get函数

读取操作,不需要同步控制,比较简单

  1. 空tab,直接返回null
  2. 计算hash值,找到相应的bucket位置,为node节点直接返回,否则返回null
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)))
                return e.val;
        }
        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;
        }
    }
    return null;
}

参考链接:
https://blog.csdn.net/programmer_at/article/details/79715177#141-addcount
https://blog.csdn.net/u010723709/article/details/48007881
https://www.jianshu.com/p/c0642afe03e0
http://cmsblogs.com/?p=2283

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值