ConcurrentHashMap深入分析(特别重要)

 前言

之前有写过ConcurrentHashMap的分析:ConcurrentHashMap源码(JDK1.8)
非常建议看一看,有些方面比本篇内容要讲的详细,本篇内容可以看成是这篇文章的补充(除了第一节put方法)

本章重点

  • 请简述解决hash冲突的方法
  • 什么是高低位迁移,它的好处是什么?

  • cas在哪些地方用到?

一、put方法第一阶段

其实之前有写过put方法的解析:

废话不多说,先贴putVal代码:

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;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

接下来我们从总体角度分析代码:

从上面的图可以看到,这段代码总体分为5个部分:

  1. 如果tab没有初始化——则调用initTable()方法初始化tab
  2. 如果tab初始化过了(1条件不成立),计算新增节点在tab中的位置的节点并赋值给f,如果f为null——用cas方式替换替换该位置的值,如果成功,跳出循环
  3. 12条件不成立,则判断f节点的是否正在迁移(hash值为-1),如果是——则帮助它迁移
  4. 123条件都不成立,表示f节点上已经有其他元素了——追加节点,有两种情况,一种是链表结构,一种是红黑树结构
  5. 调用addCount方法,该方法是增加并计算ConcurrentHashMap所有的元素总和

 好了,上面是putVal方法总体的过程分解。需要注意的是:1234部分是在死循环中进行的,即未找到出口会一直执行下去

接下来我们详细分析每个部分。

1.1 第1部分——初始化部分

我们先看一下第一部分之前的代码,并作解析:

if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
  1. 不接受空key和空value,否则抛异常
  2. 算出key的hash值
  3. binCount是check的传参:
    只要属于新增,就为整数,新增操作时,在链表结构中,binCount为遍历个数;
    在树形结构,则固定为2

 接下来分析第一部分代码:

很简单,就是tab未初始化就初始化tab。tab结构为:

transient volatile Node<K,V>[] table;

下面看一下:

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
  1. 一个线程进来sizeCtl是默认为0,不会小于0的,所以会走到else if里的。它是用cas方式将SIZECTL位置的值与sc作比较,如果相同,将其改为-1。改成-1之后
  2. 创建Node数组,数组长度默认16,将数组赋值给table,并将sc的设为16*0.75。最终sc赋值给sizeCtl
  3. 在上面几个步骤中间,如果有其他线程进来,将会走到if里,线程将会休眠,根据代码得知,唤醒后如果tab不为null,则返回table,如果还是为null且sizeCtl还是小于0,继续休眠

下面先看一下参数介绍:

  • sizeCtl——数组阈值,超过这个值就扩容
  • table——存放node的数组
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,

此方法是Java的native方法,并不由Java语言实现。
方法的作用是,读取传入对象o在内存中偏移量为offset位置的值与期望值expected作比较。相等就把x值赋值给offset位置的值。方法返回true。
不相等,就取消赋值,方法返回false。
这也是CAS的思想,及比较并交换。用于保证并发时的无锁并发的安全性。

 1.2 第二部分:如果应插入数组下标的第一个元素为空,则赋值

这部分比较简单:

  1. i=(n-1)&hash,i是该节点在数组中的下标,f是数组中i位置的第一个node元素
  2. 如果数组第一个元素为null,则创建该元素Node节点,并用cas替换原来的null值

1.3 第三部分:如果该下标位置正在迁移,则帮助它迁移

  1. 如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩张的数据复制阶段
  2. 则当前线程也会参与去复制,通过允许多线程复制的功能,一次来减少数组的复制所带来的性能损失

helpTransfer方法比较复杂,后面单独分析。

1.4 第四部分:如果上述条件都不成立,表示在这个位置有元素

else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
  1. 首先下面这些操作是在同步锁中进行的,要判断f位置的元素是链表结构还是树结构
  2. 如果是链表结构,设置binCount设置为1,并且for循环的最后一个条件++binCount。
    如果找到了key和key的hash值都一样的节点,则把它的值替换到。
    如果没找到并将新增的key-value新建节点设置到链表的尾部——尾插法
  3. 如果是树形结构,binCount设置为2,将新增的key-value新建节点插入到树结构中
  4. 最后一步,调用计数方法:addCount(1L, binCount);
    将当前 ConcurrentHashMap 的元素数量加 1, 有可能触发 transfer 操作(扩容)

binCount作为check的传参:

  • 只要属于新增则为正数,新增操作时,在链表结构中,binCount为遍历的个数
  • 在属性结构中,则固定为2

synchronized锁的作用: 

synchronized锁。这里要注意,不会出现桶正在resize的过程中执行插入,因为桶resize的时候也请求了synchronized锁。即如果该桶正在resize,这里会发生锁等待。

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

1.6 图解

addCount方法着重介绍 。

二、put方法第二阶段(元素个数的统计和更新)

2.1 addCount方法源码分析

在putVal方法执行完成以后, 会通过addCount来增加ConcurrentHashMap中的元素个数,并且还会可能触发扩容操作。这里会有两个非常经典的设计

  1. 高并发下的扩容
  2. 如何保证 addCount 的数据安全性以及性能

在 putVal 最后调用 addCount 的时候,传递了两个参数,分别是 1 和 binCount(链表长度),看看 addCount 方法里面做了什么操作x 表示这次需要在表中增加的元素个数, check 参数表示是否需要进行扩容检查,大于等于 0都需要进行检查。

先贴上addCount方法代码:

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        //判断 counterCells 是否为空,
        //1. 如果为空,就通过 cas 操作尝试修改 baseCount 变量,对这个变量进行原子累加操作(做这个操作的意义是:如果在没有竞争的情况下,仍然采用 baseCount 来记录元素个数)
        //2. 如果 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
            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 方法;
                //fullAddCount内部:Random 在线程并发的时候会有性能问题以及可能会产生相同的随机数,ThreadLocalRandom.getProbe 可以解决这个问题,并且性能要比 Random 高
                fullAddCount(x, uncontended);
                return;
            }
            ///链表长度小于等于 1, 不需要考虑扩容
            if (check <= 1)
                return;
            //统计 ConcurrentHashMap 元素个数
            s = sumCount();
        }

        // 如果需要检查,检查是否需要扩容,在 putVal 方法调用时,默认就是要检查的。
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            // 如果map.size() 大于 sizeCtl(达到扩容阈值需要扩容) 且
            // table 不是空;且 table 的长度小于 1 << 30。(可以扩容)
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                     // 如果 sc 的低 16 位不等于 标识符(校验异常 sizeCtl 变化了)
                    // 如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
                    // 如果 sc == 标识符 + 65535(帮助线程数已经达到最大)
                    // 如果 nextTable == null(结束扩容了)
                    // 如果 transferIndex <= 0 (转移状态变化了)
                    // 结束循环
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    // 如果可以帮助扩容,那么将 sc 加 1. 表示多了一个线程在帮助扩容
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                // 如果不在扩容,将 sc 更新:标识符左移 16 位 然后 + 2. 也就是变成一个负数。高 16 位是标识符,低 16 位初始是 2.
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                //计数
                s = sumCount();
            }
        }
    }

addCount代码分为两个部分:

  1. 计算节点数组中节点的总个数
  2. 扩容检测和扩容

总结一下该方法的逻辑:

x 参数表示的此次需要对表中元素的个数加几。check 参数表示是否需要进行扩容检查,大于等于0 需要进行检查,而我们的 putVal 方法的 binCount 参数最小也是 0 ,因此,每次添加元素都会进行检查。(除非是覆盖操作)

  1. 判断计数盒子属性是否是空,如果是空,就尝试修改 baseCount 变量,对该变量进行加 X。
  2. 如果计数盒子不是空,或者修改 baseCount 变量失败了,则放弃对 baseCount 进行操作。
  3. 如果计数盒子是 null 或者计数盒子的 length 是 0,或者随机取一个位置取于数组长度是 null,那么就对刚刚的元素进行 CAS 赋值。
  4. 如果赋值失败,或者满足上面的条件,则调用 fullAddCount 方法重新死循环插入。
  5. 这里如果操作 baseCount 失败了(或者计数盒子不是 Null),且对计数盒子赋值成功,那么就检查 check 变量,如果该变量小于等于 1. 直接结束。否则,计算一下 count 变量。
  6. 如果 check 大于等于 0 ,说明需要对是否扩容进行检查。
  7. 如果 map 的 size 大于 sizeCtl(扩容阈值),且 table 的长度小于 1 << 30,那么就进行扩容。
  8. 根据 length 得到一个标识符,然后,判断 sizeCtl 状态,如果小于 0 ,说明要么在初始化,要么在扩容。
  9. 如果正在扩容,那么就校验一下数据是否变化了(具体可以看上面代码的注释)。如果检验数据不通过,break。
  10. 如果校验数据通过了,那么将 sizeCtl 加一,表示多了一个线程帮助扩容。然后进行扩容。
  11. 如果没有在扩容,但是需要扩容。那么就将 sizeCtl 更新,赋值为标识符左移 16 位 —— 一个负数。然后加 2。 表示,已经有一个线程开始扩容了。然后进行扩容。然后再次更新 count,看看是否还需要扩容。

位运算(&)效率要比取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。1a % b == a & (b - 1)

前提:b 为 2^n

https://blog.csdn.net/weixin_31556371/article/details/114100984

2.1.1 总结一下

总结下来看,addCount 方法做了 2 件事情:

  1. 对ConcurrentHashMap计数+1。无论是通过修改 baseCount,还是通过使用 CounterCell。当 CounterCell 被初始化了,就优先使用他,不再使用 baseCount。

  2. 检查是否需要扩容,或者是否正在扩容。如果需要扩容,就调用扩容方法,如果正在扩容,就帮助其扩容。

有几个要点注意:

  1. 第一次调用扩容方法前,sizeCtl 的低 16 位是加 2 的,不是加一。所以 sc == rs + 1 的判断是表示是否完成任务了。因为完成扩容后,sizeCtl == rs + 1。

  2. 扩容线程最大数量是 65535,是由于低 16 位的位数限制。

  3. 这里也是可以帮助扩容的,类似 helpTransfer 方法。

2.2 CounterCells 解释 

ConcurrentHashMap 是采用 CounterCell 数组来记录元素个数的,像一般的集合记录集合大小,直接定义一个 size 的成员变量即可,当出现改变的时候只要更新这个变量就行。为什么
ConcurrentHashMap 要用这种形式来处理呢?
问题还是处在并发上, ConcurrentHashMap 是并发集合,如果用一个成员变量来统计元素个数的话,为了保证并发情况下共享变量的的安全性,势必会需要通过加锁或者自旋来实现,如果竞争比较激烈的情况下, size 的设置上会出现比较大的冲突反而影响了性能,所以在
ConcurrentHashMap 采用了分片的方法来记录大小,具体什么意思,我们来分析下:

private transient volatile int cellsBusy;// 标识当前 cell 数组是否在初始化或扩容中的CAS 标志位

/**
* Table of counter cells. When non-null, size is a power of 2.
*/
private transient volatile CounterCell[] counterCells;// counterCells 数组,总数值的分值分别存在每个 cell 中
@sun.misc.Contended static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

//看到这段代码就能够明白了, CounterCell 数组的每个元素,都存储一个元素个数,而实际我们调用size 方法就是通过这个循环累加来得到的
//又是一个设计精华,大家可以借鉴; 有了这个前提,再会过去看 addCount 这个方法,就容易理解一些了
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;
        }
    }
    return sum;
}

2.3 fullAddCount 源码分析 

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();
            // 由于重新生成了 probe,未冲突标志位设置为 true
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {//自旋
            CounterCell[] as; CounterCell a; int n; long v;
            //说明 counterCells 已经被初始化过了, 我们先跳过这个代码,先看初始化部分
            if ((as = counterCells) != null && (n = as.length) > 0) {
                //通过该值与当前线程 probe 求与,获得cells 的下标元素,和 hash 表获取索引是一样的
                //如果该下标下元素为null
                if ((a = as[(n - 1) & h]) == null) {
                    //cellsBusy=0 表示 counterCells 不在初始化或者扩容状态下
                    if (cellsBusy == 0) {            // Try to attach new Cell
                        //构造一个 CounterCell 的值,传入元素个数
                        CounterCell r = new CounterCell(x); // Optimistic create
                        //通过 cas 设置 cellsBusy 标识,防止其他线程来对 counterCells 并发处理
                        if (cellsBusy == 0 &&
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            //created-是否已创建元素标识
                            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;
                            //说明指定 cells 下标位置的数据不为空,则进行下一次循环
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                //说明在 addCount 方法中 cas 失败了,并且获取 probe 的值不为空
                else if (!wasUncontended)       // CAS already known to fail
                    //设置为未冲突标识,进入下一次自旋
                    wasUncontended = true;      // Continue after rehash
                //由于指定下标位置的 cell 值不为空,则直接通过 cas 进行原子累加,如果成功,则直接退出
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                //如果已经有其他线程建立了新的 counterCells 或者 CounterCells 大于 CPU 核心数
(很巧妙,线程的并发数不会超过 cpu 核心数)
                else if (counterCells != as || n >= NCPU)
                    //如果已经有其他线程建立了新的 counterCells 或者 CounterCells 大于 CPU 核心数(很巧妙,线程的并发数不会超过 cpu 核心数)
                    collide = false;            // At max size or stale
                //恢复 collide 状态,标识下次循环会进行扩容
                else if (!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;                   // Retry with expanded table
                }
                //更新随机数的值
                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) {
                        //初始化容量为 2
                        CounterCell[] rs = new CounterCell[2];
                        //将 x 也就是元素的个数放在指定的数组下标位置
                        rs[h & 1] = new CounterCell(x);
                        //赋值给 counterCells
                        counterCells = rs;
                        //设置初始化完成标识
                        init = true;
                    }
                } finally {
                    //恢复标识
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            //竞争激烈,其它线程占据 cell 数组,直接累加在 base 变量中
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

 下面两张图,是上面代码的总览:

 2.4 CounterCells 初始化图解

初始化长度为 2 的数组,然后随机得到指定的一个数组下标,将需要新增的值加入到对应下标位置处:

三、transfer 扩容阶段

  • 当元素个数大于阈值的时候
  • 如果此时正在扩容, 在扩容阶段进来的线程会协助扩容

3.1 扩容代码总览 

 

 3.2 高低位

 tabAt(tab, i = (n - 1) & hash))

假设: table=16 -1 -> 0000 1111
假设hash =9 , 0000 1001 -> (n-1) & hash
0000 1111 & 0000 1001 =0000 1001 -> 9
扩容之后, 16~32 .
0001 1111
hash=9 -> 9
当一个值的hash=9的时候, 在16位,32位。 -> hash =20

 3.3 transfer 扩容阶段

判断是否需要扩容,也就是当更新后的键值对总数 baseCount >= 阈值 sizeCtl 时,进行rehash,这里面会有两个逻辑。

  1. 如果当前正在处于扩容阶段,则当前线程会加入并且协助扩容
  2. 如果当前没有在扩容,则直接触发扩容操作
//如果 binCount>=0,标识需要检查扩容
if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            //s 标识集合大小,如果集合大小大于或等于扩容阈值(默认值的 0.75)
            //并且 table 不为空并且 table 的长度小于最大容量
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                /这里是生成一个唯一的扩容戳,这个是干嘛用的呢? 且听我后面慢慢分析
                int rs = resizeStamp(n);
                //sc<0,也就是 sizeCtl<0,说明已经有别的线程正在扩容了
                if (sc < 0) {
                    //这 5 个条件只要有一个条件为 true,说明当前线程不能帮助进行此次的扩容,直接跳出循环
                    //sc >>> RESIZE_STAMP_SHIFT!=rs 表示比较高 RESIZE_STAMP_BITS 位生成戳和 rs 是否相等,相同
                    //sc=rs+1 表示扩容结束
                    //sc==rs+MAX_RESIZERS 表示帮助线程线程已经达到最大值了
                    //nt=nextTable -> 表示扩容已经结束
                    //transferIndex<=0 表示所有的 transfer 任务都被领取完了,没有剩余的hash 桶来给自己自己好这个线程来做 transfer
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    //当前线程尝试帮助此次扩容,如果成功,则调用 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();
            }
        }
    }

 3.4 resizeStamp

这块逻辑要理解起来,也有一点复杂。
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 00000000
然后再+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

        扩容是 ConcurrentHashMap 的精华之一, 扩容操作的核心在于数据的转移,在单线程环境下数据的转移很简单,无非就是把旧数组中的数据迁移到新的数组。但是这在多线程环境下,在扩容的时候其他线程也可能正在添加元素,这时又触发了扩容怎么办? 可能大家想到的第一个解决方案是加互斥锁,把转移过程锁住,虽然是可行的解决方案,但是会带来较大的性能开销。因为互斥锁会导致所有访问临界区的线程陷入到阻塞状态,持有锁的线程耗时越长,其他竞争线程就会一直被阻塞,导致吞吐量较低。而且还可能导致死锁。

       而 ConcurrentHashMap 并没有直接加锁,而是采用 CAS 实现无锁的并发同步策略,最精华
的部分是它可以利用多线程来进行协同扩容。

       简单来说,它把 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
        //nextTab 未初始化, nextTab 是用来扩容的 node 数组
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                //新建一个 n<<1 原始 table 大小的 nextTab,也就是 32
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                //赋值给 nextTab
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                //扩容失败, sizeCtl 使用 int 的最大值
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            //更新成员变量
            nextTable = nextTab;
            //更新转移下标, 表示转移时的下标
            transferIndex = n;
        }
        //新的 tab 的长度
        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; // to ensure sweep before committing nextTab
        //通过 for 自循环处理每个槽位中的链表元素,默认 advace 为真,通过 CAS 设置
transferIndex 属性值,并初始化 i 和 bound 值, i 指当前处理的槽位序号, bound 指需要处理的槽位边界,先处理槽位 15 的节点;
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                //--i 表示下一个待处理的 bucket,如果它>=bound,表示当前线程已经分配过bucket 区域
                if (--i >= bound || finishing)
                    advance = false;
                //表示所有 bucket 已经被分配完毕
                else if ((nextIndex = transferIndex) <= 0) {
                    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;//0
                    i = nextIndex - 1;//15
                    advance = false;
                }
            }
            //i<0 说明已经遍历完旧的数组,也就是当前线程已经处理完所有负责的 bucket
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                //如果完成了扩容
                if (finishing) {
                    //删除成员变量
                    nextTable = null;
                    //更新 table 数组
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);//更新阈值(32*0.75=24)
                    return;
                }
                // sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2 (详细介绍点击这里)
                // 然后, 每增加一个线程参与迁移就会将 sizeCtl 加 1,
                // 这里使用 CAS 操作对 sizeCtl 的低 16 位进行减 1,代表做完了属于自己的任务
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //第一个扩容的线程,执行 transfer 方法之前,会设置 sizeCtl =(resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2)后续帮其扩容的线程,执行 transfer 方法之前,会设置 sizeCtl = sizeCtl+1
                    //每一个退出 transfer 的方法的线程,退出之前,会设置 sizeCtl = sizeCtl-1
那么最后一个线程退出时:必然有
                    //sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2),即 (sc - 2)
== resizeStamp(n) << RESIZE_STAMP_SHIFT
                    // 如果 sc - 2 不等于标识符左移 16 位。如果他们相等了,说明没有线程在帮助他们扩容了。也就是说,扩容结束了。
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    // 如果相等,扩容结束了,更新 finising 变量
                    finishing = advance = true;
                    // 再次循环检查一下整张表
                    i = n; // recheck before commit
                }
            }
            // 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 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) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

4.1 扩容过程图解

         ConcurrentHashMap 支持并发扩容,实现方式是,把 Node 数组进行拆分,让每个线程处理
自己的区域,假设 table 数组总长度是 64,默认情况下,那么每个线程可以分到 16 个 bucket。

然后每个线程处理的范围,按照倒序来做迁移:
通过 for 自循环处理每个槽位中的链表元素,默认 advace 为真,通过 CAS 设置 transferIndex
属性值,并初始化 i 和 bound 值, i 指当前处理的槽位序号 bound 指需要处理的槽位边界
先处理槽位 31 的节点; (bound,i) =(16,31) 从 31 的位置往前推动。

假设这个时候 ThreadA 在进行 transfer,那么逻辑图表示如下:



 在当前假设条件下 ,槽位 15 中没有节点,则通过 CAS 插入在第二步中初始化的
ForwardingNode 节点,用于告诉其它线程该槽位已经处理过了;

4.2 sizeCtl 扩容退出机制

在扩容操作 transfer 的第 2414 行,代码如下:

if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)

每存在一个线程执行完扩容操作,就通过 cas 执行 sc-1。

接着判断(sc-2) !=resizeStamp(n) << RESIZE_STAMP_SHIFT ; 如果相等,表示当前为整个扩
容操作的 最后一个线程,那么意味着整个扩容操作就结束了;如果不想等,说明还得继续这么做的目的,一方面是防止不同扩容之间出现相同的 sizeCtl,另外一方面,还可以避免sizeCtl 的 ABA 问题导致的扩容重叠的情况。

五、数据迁移阶段的实现分析

通过分配好迁移的区间之后,开始对数据进行迁移。 在看这段代码之前, 先来了解一下原理。

//对数组该节点位置加锁,开始处理数组该位置的迁移工作
synchronized (f) {
                    //再做一次校验
                    if (tabAt(tab, i) == f) {
                        //ln 表示低位, hn 表示高位;接下来这段代码的作用是把链表拆分成两部分, 0 在低位, 1 在高位
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            //遍历当前 bucket 的链表,目的是尽量重用 Node 链表尾部的一部分
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            //如果最后更新的 runBit 是 0,设置低位节点
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            //否则,设置高位节点
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            //构造高位以及低位的链表
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            //将低位的链表放在 i 位置也就是不动
                            setTabAt(nextTab, i, ln);
                            //将高位链表放在 i+n 位置
                            setTabAt(nextTab, i + n, hn);
                            // 把旧 table 的 hash 桶中放置转发节点,表明此 hash 桶已经被处理
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        //红黑树的扩容部分暂时忽略
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }

 5.1 高低位原理分析

图解:

ConcurrentHashMap 在做链表迁移时,会用高低位来实现,这里有两个问题要分析一下:

 1. 如何实现高低位链表的区分
假如我们有这样一个队列

         第 14 个槽位插入新节点之后,链表元素个数已经达到了 8,且数组长度为 16,优先通过扩容来缓解链表过长的问题,扩容这块的图解稍后再分析,先分析高低位扩容的原理:
        假如当前线程正在处理槽位为 14 的节点,它是一个链表结构,在代码中,首先定义两个变量
节点 ln 和 hn,实际就是 lowNode 和 HighNode,分别保存 hash 值的第 x 位为 0 和不等于
0 的节点。
        通过 fn&n 可以把这个链表中的元素分为两类, A 类是 hash 值的第 X 位为 0, B 类是 hash 值的第 x 位为不等于 0(至于为什么要这么区分,稍后分析),并且通过 lastRun 记录最后要处
理的节点。最终要达到的目的是, A 类的链表保持位置不动, B 类的链表为 14+16(扩容增加
的长度)=30。
我们把 14 槽位的链表单独伶出来,我们用蓝色表示 fn&n=0 的节点,假如链表的分类是这
样:

for (Node<K,V> p = f.next; p != null; p = p.next) {
    int b = p.hash & n;
    if (b != runBit) {
        runBit = b;
        lastRun = p;
    }
}

        通过上面这段代码遍历,会记录 runBit 以及 lastRun,按照上面这个结构,那么 runBit 应该
是蓝色节点, lastRun 应该是第 6 个节点。
接着,再通过这段代码进行遍历,生成 ln 链以及 hn 链:

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

        接着,通过 CAS 操作,把 hn 链放在 i+n 也就是 14+16 的位置, ln 链保持原来的位置不动。
并且设置当前节点为 fwd,表示已经被当前线程迁移完了:

setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);

迁移完成以后的数据分布如下:

5.2 为什么要做高低位的划分

        要想了解这么设计的目的,我们需要从 ConcurrentHashMap 的根据下标获取对象的算法来看,在 putVal 方法中 1018 行:

(f = tabAt(tab, i = (n - 1) & hash)) == null

        通过(n-1) & hash 来获得在 table 中的数组下标来获取节点数据,【&运算是二进制运算符, 1& 1=1,其他都为 0】

假设我们的 table 长度是 16, 二进制是【0001 0000】,减一以后的二进制是 【00001111】
假如某个 key 的 hash 值=9,对应的二进制是【0000 1001】,那么按照(n-1) & hash 的算法:
0000 1111 & 0000 1001 =0000 1001 , 运算结果是 9
当我们扩容以后, 16 变成了 32,那么(n-1)的二进制是 【0001 1111】
仍然以 hash 值=9 的二进制计算为例
0001 1111 & 0000 1001 =0000 1001 ,运算结果仍然是 9
我们换一个数字,假如某个 key 的 hash 值是 20,对应的二进制是【0001 0100】,仍然按照(n-1) & hash算法,分别在 16 为长度和 32 位长度下的计算结果
16 位: 0000 1111 & 0001 0100=0000 0100
32 位: 0001 1111 & 0001 0100 =0001 0100
从结果来看,同样一个 hash 值,在扩容前和扩容之后,得到的下标位置是不一样的,这种情况当然是不允许出现的,所以在扩容的时候就需要考虑,
而使用高低位的迁移方式,就是解决这个问题.
大家可以看到, 16 位的结果到 32 位的结果,正好增加了 16。
比如 20 & 15=4 、 20 & 31=20 ; 4-20 =16
比如 60 & 15=12 、 60 & 31=28; 12-28=16
所以对于高位,直接增加扩容的长度,当下次 hash 获取数组位置的时候,可以直接定位到对应的位置。
这个地方又是一个很巧妙的设计,直接通过高低位分类以后,就使得不需要在每次扩容的时候来重新计算 hash,极大提升了效率。

5.3 不理解的地方:

  • runBit和lastRun的作用是什么?
  • 第二个红框的代码的意义是什么?
  • 为什么 if ((ph & n) == 0) 可以任务它是低位?

5.4 扩容结束以后的退出机制

如果线程扩容结束, 那么需要退出,就会执行 transfer 方法的如下代码:

//i<0 说明已经遍历完旧的数组,也就是当前线程已经处理完所有负责的 bucket
if (i < 0 || i >= n || i + n >= nextn) {
    int sc;
    //如果完成了扩容
    if (finishing) {
        //删除成员变量
        nextTable = null;
        //更新 table 数组
        table = nextTab;
        //更新阈值(32*0.75=24)
        sizeCtl = (n << 1) - (n >>> 1);
        return;
     }
     // sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
    // 然后, 每增加一个线程参与迁移就会将 sizeCtl 加 1,
    // 这里使用 CAS 操作对 sizeCtl 的低 16 位进行减 1,代表做完了属于自己的任务
     if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
        //第一个扩容的线程,执行 transfer 方法之前,会设置 sizeCtl =(resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2)
        //后续帮其扩容的线程,执行 transfer 方法之前,会设置 sizeCtl = sizeCtl+1
        //每一个退出 transfer 的方法的线程,退出之前,会设置 sizeCtl = sizeCtl-1
        //那么最后一个线程退出时:必然有
        //sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2),即 (sc - 2)== resizeStamp(n) << RESIZE_STAMP_SHIFT
        // 如果 sc - 2 不等于标识符左移 16 位。如果他们相等了,说明没有线程在帮助他们扩容了。也就是说,扩容结束了。
        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
           return;
        // 如果相等,扩容结束了,更新 finising 变量
        finishing = advance = true;
        // 再次循环检查一下整张表
        i = n; // recheck before commit
     }
}

六、put 方法第三阶段——helpTransfer

从名字上来看, 代表当前是去协助扩容:

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) {
                //下面部分的整个代码表示扩容结束,直接退出循环
                //transferIndex<=0 表示所有的 Node 都已经分配了线程
                //sc=rs+MAX_RESIZERS 表示扩容线程数达到最大扩容线程数
                //sc >>> RESIZE_STAMP_SHIFT !=rs, 如果在同一轮扩容中,那么 sc 无符号右移比较高位和 rs 的值,那么应该是相等的。如果不相等,说明扩容结束了
                //sc==rs+1 表示扩容结束
                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;
    }

七、put 方法第四阶段

这个方法的主要作用是:

  1. 如果被添加的节点的位置已经存在节点的时候,需要以链表的方式加入到节点中。
  2. 如果当前节点已经是一颗红黑树,那么就会按照红黑树的规则将当前节点加入到红黑树中。
//进入到这个分支,说明 f 是当前 nodes 数组对应位置节点的头节点,并且不为空
else {
                V oldVal = null;
                 //给对应的头结点加锁
                synchronized (f) {
                    //再次判断对应下标位置是否为 f 节点
                    if (tabAt(tab, i) == f) {
                        //头结点的 hash 值大于 0,说明是链表
                        if (fh >= 0) {
                            //用来记录链表的长度
                            binCount = 1;
                            //遍历链表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //如果发现相同的 key,则判断是否需要进行值的覆盖
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    //默认情况下,直接覆盖旧的值
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                //一直遍历到链表的最末端,直接把新的值加入到链表的最后面
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //如果当前的 f 节点是一颗红黑树
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            //则调用红黑树的插入方法插入新的值
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                //同样,如果值已经存在,则直接替换
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }

        这一块内容在之前的文章已做过分析,所以不再做解释。包括树节点的添加与树的元素统计也不作解释,因为红黑树,光看代码能理解其逻辑,但红黑的重点是算法,关于红黑树我目前掌握的不多,也仅有一些基本的了解。 

八、ConcurrentHashMap jdk1.8和jdk1.7的区别

        ConcurrentHashMap 和 HashMap 的实现原理是差不多的,但是因为 ConcurrentHashMap
需要支持并发操作,所以在实现上要比 hashmap 稍微复杂一些。
        在 JDK1.7 的 实 现 上 , ConrruentHashMap 由 一 个 个 Segment 组 成 , 简 单 来 说 ,
ConcurrentHashMap 是一个 Segment 数组,它通过继承 ReentrantLock 来进行加锁,通过
每次锁住一个 segment 来保证每个 segment 内的操作的线程安全性从而实现全局线程安全。
整个结构图如:

        当每个操作分布在不同的 segment 上的时候,默认情况下,理论上可以同时支持 16 个线程的并发写入。

相比于 1.7 版本,它做了两个改进:

  1. 取消了 segment 分段设计,直接使用 Node 数组来保存数据,并且采用 Node 数组元素作为锁来实现每一行数据进行加锁来进一步减少并发冲突的概率
  2. 将原本数组+单向链表的数据结构变更为了数组+单向链表+红黑树的结构。为什么要引入红黑树呢?在正常情况下, key hash 之后如果能够很均匀的分散在数组中,那么 table 数组中的每个队列的长度主要为 0 或者 1.但是实际情况下,还是会存在一些队列长度过长的情况。如果还采用单向列表方式,那么查询某个节点的时间复杂度就变为 O(n); 因此对于队列长度超过 8 的列表, JDK1.8 采用了红黑树的结构,那么查询的时间复杂度就会降低到O(logN),可以提升查找的性能;


        这个结构和 JDK1.8 版本中的 Hashmap 的实现结构基本一致,但是为了保证线程安全性,
ConcurrentHashMap 的实现会稍微复杂一下。接下来我们从源码层面来了解一下它的原理。我们基于 put 和 get 方法来分析它的实现即可

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值