尝试分析J.U.C的ConcurrentHashMap

尝试分析J.U.C的ConcurrentHashMap(JDK1.8)

ConcurrentHashMap的初步使用及场景

ConcurrentHashMap 是 J.U.C 包里面提供的一个线程安全并且高效的 HashMap,所以 ConcurrentHashMap 在并发编程的场景中使用的频率比较高

api使用

ConcurrentHashMap 是 Map 的派生类,所以 api 基本和 Hashmap 是类似,主要就是 put、 get 这些方法,接下来基于 ConcurrentHashMap 的 put 和 get 这两个方法作为切入点来分 析 ConcurrentHashMap 的源码实现

ConcurrentHashMap的源码分析

1.7 版本

在 JDK1.7 的 实 现 上 , ConrruentHashMap 由一个个 Segment 组成 , 简单来说 , ConcurrentHashMap 是一个 Segment 数组,它通过继承 ReentrantLock 来进行加锁,通过每次锁住一个 segment 来保证每个 segment 内的操作的线程安全性从而实现全局线程安全。

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

1.8版本

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

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

类属性变量

// 存放数据的桶
transient volatile Node<K,V>[] table;
// 扩容时的桶  
private transient volatile Node<K,V>[] nextTable;
// 保存桶内基本元素个数,在无竞争时,才会修改
private transient volatile long baseCount;
// 保存桶内元素个数,在并发时,才会修改
private transient volatile CounterCell[] counterCells;
// CounterCell[] 数组的元素个数
private transient volatile int cellsBusy;

// 默认为0,用来控制table的初始化和扩容操作
// -1 代表table正在初始化
// -N 表示有N-1个线程正在进行扩容操作(取-N对应的二进制的低16位数值为M,此时有M-1个线程进行扩容)
// 其余情况:
// 1、如果table未初始化,表示table需要初始化的大小。
// 2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍
private transient volatile int sizeCtl;
// 调整大小时要分割的下一个表索引
private transient volatile int transferIndex;

put方法第一阶段(初始化)

private transient volatile int sizeCtl;

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()); // 计算key的hash值
    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(); // 初始化数组
            
        // 通过 hash 值对应的数组下标得到第一个节点; 以 volatile 读的方式来读取 table 数组中的元素,保证每次拿到的数据都是最新的
        // i(桶的下标)
        } 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
        }
// -------------------------- 第一阶段 end --------------------------------
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
          	// 加锁后,进行node数据修改
            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;
}

// 计算hash值
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
// 初始化数组
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) { // 自旋操作
        if ((sc = sizeCtl) < 0) // 当有一个线程进入初始化阶段,修改sizeCtl=-1后,则其他线程进入Thread.yield(); 让出CPU执行权给初始化ConcurrentHashMap线程
            Thread.yield(); 
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 尝试把sizeCtl变量设置-1,如果成功,则ConcurrentHashMap进入初始化阶段。
            // 只能有一个线程进入初始化阶段
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 默认初始化数组大小16
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2); // 计算下次扩容的阈值,实际就是当前容量的0.75倍,这里使用了右移来计算
                }
            } finally {
                sizeCtl = sc; //设置 sizeCtl 为 sc, 如果默认是 16 的话,那么这个时候sc=16*0.75=12
            }
            break;
        }
    }
    return tab;
}

// 直接操作主内存,拿去桶下标元素,等价于tab[i]
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);
}
// 直接操作主内存,设置下标的元素,等价于 tab[i] = v
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);
}

假如在上面这段代码中存在两个线程,在不加锁的情况下:线程 A 成功执行 casTabAt 操作后,随后的线程 B 可以通过 tabAt 方法立刻看到 table[i]的改变。原因如下:线程 A 的 casTabAt 操作,具有 volatile 读写相同的内存语义,根据 volatile 的 happens-before 规则:线程 A 的 casTabAt 操作,一定对线程 B 的 tabAt 操作可见

tabAt

该方法获取对象中offset偏移地址对应的对象field的值。实际上这段代码的含义等价于tab[i], 但是为什么不直接使用 tab[i]来计算呢?

getObjectVolatile,一旦看到 volatile 关键字,就表示可见性。因为对 volatile 写操作 happenbefore 于 volatile 读操作,因此其他线程对 table 的修改均对 get 读取可见; 虽然 table 数组本身是增加了 volatile 属性,但是“volatile 的数组只针对数组的引用具有 volatile 的语义,而不是它的元素”。 所以如果有其他线程对这个数组的元素进行写操作,那 么当前线程来读的时候不一定能读到最新的值。

出于性能考虑,Doug Lea 直接通过 Unsafe 类来对 table 进行操作。

put方法第二阶段(hash冲突)

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

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

在 putVal 最后调用 addCount 的时候,传递了两个参数,分别是 1 和 binCount(链表长度), 看看 addCount 方法里面做了什么操作。

    //将当前 ConcurrentHashMap 的元素数量加 1,有可能触发 transfer 操作(扩容)
     addCount(1L, binCount);
     return null;
}

/**
 * @param x 表示这次需要在表中增加的元素个数
 * @param check 参数表示是否需要进行扩容检查,大于等于 0 都需要进行检查
 */
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; // 是否冲突标识,(true.没有冲突,false.有冲突)默认为没有冲突
      
        // 1.计数表(CounterCell[])为空则直接调用 fullAddCount
        // 2.从计数表(CounterCell[])中随机取出一个数组的位置为空,直接调用 fullAddCount。(这里又用到了一种巧妙的方法,调用 fullAndCountRandom 在线程并发的时候会有性能问题以及可能会产生相同的随机数,ThreadLocalRandom.getProbe 可以解决这个问题,并且性能要比 Random 高)
        // 3. 通过 CAS 修改 CounterCell 随机位置的值,如果修改失败说明出现并发情况
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !( uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x) )
         ) {
          	// 初始化 CounterCell
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1) // /链表长度小于等于 1,不需要考虑扩容
            return;
        s = sumCount(); // 统计 ConcurrentHashMap 元素个数
    }
    if (check >= 0) { // 是否进行扩容检查
        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);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}
CounterCells解释

ConcurrentHashMap 是采用 CounterCell 数组来记录元素个数的,像一般的集合记录集合大小,直接定义一个 size 的成员变量即可,当出现改变的时候只要更新这个变量就行。为什么 ConcurrentHashMap 要用这种形式来处理呢?

问题还是处在并发上,ConcurrentHashMap 是并发集合,如果用一个成员变量来统计元素个数的话,为了保证并发情况下共享变量的线程安全修改,势必会需要通过加锁或者自旋来实现, 如果竞争比较激烈的情况下,size 的设置上会出现比较大的冲突反而影响了性能,所以在 ConcurrentHashMap 采用了分片的方法来记录大小。

@sun.misc.Contended static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

// 看到这段代码就能够明白了,CounterCell 数组的每个元素,都存储一个元素个数,而实际我们调用 size 方法就是通过这个循环累加来得到的
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;
}
fullAddCount源码分析

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

/**
 * @param x 添加的个数
 * @param wasUncontended 是否出现并发情况
 */
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 != null, 说明 counterCells 已经被初始化过了
        if ((as = counterCells) != null && (n = as.length) > 0) {
          
          
          	
            // 1.通过该值与当前线程 probe 求与,获得 cells 的下标元素,和 hash 表获取索引是一样的
            if ((a = as[(n - 1) & h]) == null) { // 获得 cells 的下标元素,为null
                if (cellsBusy == 0) { // cellsBusy = 0 表示 counterCells 不在初始化或者扩容状态下
                    CounterCell r = new CounterCell(x); // 因为前面判断下标元素为null,构造一个 CounterCell 的值,传入元素个数
                    // 通过 cas 设置 cellsBusy 标识,防止其他线程来对 counterCells 并发处理
                    if (cellsBusy == 0 && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                      	// 初始化单个CounterCell,并放入CounterCells数组中,cellsBusy用来控制线程进来,有类似锁的概念
                        boolean created = false;
                        try {
                            CounterCell[] rs; int m, j;
                            if ((rs = counterCells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                            cellsBusy = 0; // 恢复标志位
                        }
                        if (created) // /创建成功,退出循环
                            break;
                        continue;     //说明指定 cells 下标位置的数据不为空,则进行下一次循环
                    }
                }
                collide = false; // 设置当前线程的循环失败不进行扩容
            }
          
          
          
            // 说明在 addCount 方法中 cas 失败了(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)),并且获取 probe 的值不为空
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // 设置为未冲突标识,进入下一次自旋
          
          
            // 2.由于指定下标位置的 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;
          
          
            // 3.进入这个步骤,说明 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); // 更新随机数的值
        }
        // counterCells == null, cellsBusy = 0表示没有在做初始化,设置标记,进行扩容或者初始化
        else if (cellsBusy == 0 && counterCells == as &&
                 U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
            // 设置标记位cellsBusy = 1后,说明这段代码目前只能有一个线程执行
            boolean init = false;
            try {
                if (counterCells == as) { // 判断counterCells是否有其他线程已经执行初始化
                    CounterCell[] rs = new CounterCell[2];//初始化容量为 2
                    rs[h & 1] = new CounterCell(x);//将 x 也就是元素的个数放在指定的数组下标位置
                    counterCells = rs;
                    init = true; //设置初始化完成标识
                }
            } finally {
                cellsBusy = 0; //恢复标识
            }
            if (init)
                break;
        }
        //竞争激烈,其它线程占据 cell 数组,到最后,尝试再次累加在baseCount变量中,可能会成功,优化执行
        else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
            break;                          // 如果成功就跳出自旋操作
    }
}
CountCells 初始化图解

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

transfer扩容阶段

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

  1. 如果当前正在处于扩容阶段,则当前线程会加入并且协助扩容
  2. 如果当前没有在扩容,则直接触发扩容操作
private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
    if (check >= 0) { // 如果 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);   // 这里是生成一个唯一的扩容戳,
            if (sc < 0) { // sc < 0,也就是 sizeCtl < 0,说明已经有别的线程正在扩容了, 扩容时,sc左移16位,高位为1代表负数 以长度为16的数组,正在扩容时状态为 [1000 0000 0001 1011 0000 0000 0000 0010]
                
                //这 5 个条件只要有一个条件为 true,说明当前线程不能帮助进行此次的扩容,直接跳出循环
                // 1. sc >>> RESIZE_STAMP_SHIFT != rs 表示比较高 RESIZE_STAMP_BITS 位生成戳和 rs 不相等
                // 2. sc == rs + 1 表示扩容结束
                // 3. sc == rs + MAX_RESIZERS 表示帮助线程线程已经达到最大值了
                // 4. (nt = nextTable) == null  表示扩容已经结束
                // 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;
              
              
              	// 当前线程尝试协助扩容,如果+1成功,则调用 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

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

private static int RESIZE_STAMP_BITS = 16;

// n = tab.length 数组长度
static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

Integer.numberOfLeadingZeros 这个方法是返回32位无符号整数 n 最高位非 0 位前面的 0 的个数

比如 10 的二进制是 0000 0000 0000 0000 0000 0000 0000 1010

那么这个Integer.numberOfLeadingZeros方法返回的值就是 28,因为前面有28个0。

根据 resizeStamp 的运算逻辑,我们来推演一下,假如 n=16, Integer.numberOfLeadingZeros(16)= 27,那么 resizeStamp(16)= 32795

resizeStamp(16) = Integer.numberOfLeadingZeros(n) | (1 << (16 - 1))
# Integer.numberOfLeadingZeros(16) = 0000 0000 0000 0000 0000 0000 0001 1011 返回无符号整数 n 最高位非 0 位前面的 0 的个数
resizeStamp(16) = 27 | 32768

  0000 0000 0000 0000 0000 0000 0001 1011
| 0000 0000 0000 0000 1000 0000 0000 0000
= 0000 0000 0000 0000 1000 0000 0001 1011  = 32795

接着再来看,当第一个线程尝试进行扩容的时候,会执行下面这段代码

U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)

rs(扩容戳) 左移 16 位,相当于原本的二进制低位变成了高位 1000 0000 0001 1011 0000 0000 0000 0000

然后再+2 = 1000 0000 0001 1011 0000 0000 0000 0000 + 10 = 1000 0000 0001 1011 0000 0000 0000 0010

此时sc = rs << RESIZE_STAMP_SHIFT) + 2,高位第一位为1,所以是负值。

高 16 位代表扩容的标记、低 16 位代表并行扩容的线程数

高RESIZE_STAMP_BITS位低RESIZE_STAMP_SHIFT位
当前长度下的扩容标记(唯一的)参与扩容线程数
  • 这样来存储有什么好处呢?
    1. 首先在 ConcurrentHashMap 中是支持并发扩容的,也就是说如果当前的数组需要进行扩容操作,可以由多个线程来共同负责
    2. 可以保证每次扩容都生成唯一的生成戳,每次新的扩容,都有一个不同的 n,这个生成戳就是根据 n 来计算出来的一个数字,n 不同,这个数字也不同
  • 第一个线程尝试扩容的时候,为什么是+2 ?
    1. 因为 -1 表示初始化(SIZECTL = -1时初始化标记 1000 0000 0000 0000 0000 0000 0000 0001),-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; // stride 每个线程操作的区间长度
  
    // 将 (n>>>3 相当于 n/8) 然后除以 CPU 核心数。如果得到的结果小于 16,那么就使用 16
    // 这里的目的是让每个 CPU 处理的桶一样多,避免出现转移任务不均匀的现象,如果桶较少的话,默认一个 CPU(一个线程)处理 16 个node,也就是长度为 16 的时候,扩容的时候只会有一个线程来扩容。 MIN_TRANSFER_STRIDE = 16
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // 默认每个线程处理扩容区间只能有16个node
    if (nextTab == null) {   // nextTab 未初始化,nextTab 是用来扩容的 node 数组
        try {
            @SuppressWarnings("unchecked")
            // 新建一个 n<<1 原始 table 大小的 nextTab,也就是 16 << 1 = 32
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt; // 赋值给 nextTab
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE; // 扩容失败,sizeCtl 使用 int 的最大值
            return;
        }
        nextTable = nextTab; // 更新成员变量
        transferIndex = n; // 更新转移下标,表示转移时的下标,原来的table.length
    }
    int nextn = nextTab.length; // 新的 tab 的长度
    // 创建一个 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;;) {
        // 这个循环使用 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)
          	// 如果当64长度扩容到128长度 当前最多只能有三个线程执行 (32,63)(16,31)(0,15) 区间扩容
          	// 如果当32长度扩容到64长度 当前最多只能有两个线程执行 (16,31)(0,15) 区间扩容
            // 如果当16长度扩容到32长度 当前最多只能一个线程执行 (0,15) 区间扩容
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ? nextIndex - stride : 0)
                     )
            ) {
                // 当前线程获得分配的任务区间,跳出循环,执行下面的迁移节点操作
                bound = nextBound; // 设置新的边界
                i = nextIndex - 1;
                // 16位第一个线程扩容nextBound=0,32位第一个线程扩容nextBound=16
                // 16位第一个线程扩容i=15,32位第一个线程扩容i=31
                advance = false;
            }
        }
      
      
      
        // 1. i < 0 说明已经遍历完旧的数组,也就是当前线程已经处理完所有负责的 bucket
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) { // 如果完成了扩容
                nextTable = null; // 删除成员变量
                table = nextTab; // 更新 table 数组
                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
            }
        }
      
      
      
        // 2. 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode ”空节点“
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        
        // 3. 表示该位置已经完成了迁移,也就是如果线程 A 已经处理过这个节点,那么线程 B 处理这个节点时,hash 值一定为 MOVED
        else if ((fh = f.hash) == MOVED)
            advance = true; // 已经处理过
      
      	// 4. 开始迁移
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                  	....
                }
            }
        }
    }
}
扩容过程图解

ConcurrentHashMap 支持并发扩容,实现方式是,把 Node 数组进行拆分,让每个线程处理自己的区域,假设 table 数组总长度是 32,默认情况下,那么每个线程可以分到 16 个 bucket。最多可支持2个线程并发扩容,然后每个线程处理的范围,按照倒序来做迁移。

通过 for 自循环处理每个槽位中的链表元素,默认 advace 为true,通过 CAS 设置 transferIndex 属性值,并初始化 i 和 bound 值,i 指当前处理的槽位序号,bound 指需要处理的槽位边界, 先处理槽位 31 的节点; (bound, i) =(16, 31) 从 31 的位置往前推动。

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

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

sizeCtl扩容退出机制

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

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
}

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

接着判断 (sc-2) != resizeStamp(n) << RESIZE_STAMP_SHIFT;

  • 如果相等,表示当前为整个扩容操作的最后一个线程,那么意味着整个扩容操作就结束了。
  • 如果不相等,说明还得继续,一方面是防止不同扩容之间出现相同的 sizeCtl,另外一方面,还可以避免 sizeCtl 的 ABA 问题导致的扩容重叠的情况。
数据迁移阶段的实现
else if ((fh = f.hash) == MOVED)
		advance = true; // 已经处理过
// 对数组该节点位置加锁,开始处理数组该位置的迁移工作, 在上一步 else if ((f = tabAt(tab, i)) == null) 赋值
synchronized (f) { 
    if (tabAt(tab, i) == f) { // 再做一次校验,是不是相同的链表
      	// ln 表示低位, hn 表示高位; 接下来这段代码的作用是把链表拆分成两部分,0 在低位(不需要迁移),1 在高位(需要迁移)
        Node<K,V> ln, hn; 

        if (fh >= 0) { // fh = f.hash
            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; // 记录最后一个高低位变化的Node
                }
            }
            if (runBit == 0) { //p.hash & n = 0 是低位节点
                ln = lastRun;
                hn = null;
            }
            else {   // p.hash & n != 0 是高位节点
                hn = lastRun;
                ln = null;
            }
            // 构造高位以及低位的链表
            for (Node<K,V> p = f; p != lastRun; p = p.next) { // 从头开始遍历,直到遇到lastRun停止循环
                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);//将低位的链表放在 i 位置也就是不动
            setTabAt(nextTab, i + n, hn); //将高位链表放在 i+n 位置
            setTabAt(tab, i, fwd); // 把旧 table 的 hash 桶中放置转发节点,表明此 hash 桶已经被处理
            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;
        }
    }
}
高低位原理分析

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

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

为什么要做高低位的划分

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

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

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

假设我们的 table 长度是 16, 二进制是【0001 0000】,减一以后的二进制是 【0000 1111】
假如某个 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
运算结果仍然是 4。

32 位: 
  0001 1111 
& 0001 0100 
= 0001 0100
运算结果仍然是 20。

从结果来看,同样一个 hash 值,在扩容前和扩容之后,得到的下标位置是不一样的,这种情况当然是
不允许出现的,所以在扩容的时候就需要考虑,
而使用高低位的迁移方式,就是解决这个问题.
大家可以看到,16 位的结果到 32 位的结果,正好增加了 16.
比如 20 & 15 = 4 、20 & 31 = 20; 4 - 20 = 16
比如 60 & 15 = 12 、60 & 31 = 28; 12 - 28 = 16
所以对于高位,直接增加扩容的长度,当下次 hash 获取数组位置的时候,可以直接定位到对应的位置。
这个地方又是一个很巧妙的设计,直接通过高低位分类以后,就使得不需要在每次扩容的时候来重新计算 hash,极大提升了效率。

扩容结束以后的退出机制

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

// i < 0 说明已经遍历完旧的数组,也就是当前线程已经处理完所有负责的 bucket
if (i < 0 || i >= n || i + n >= nextn) {
    int sc;
    if (finishing) {// 如果完成了扩容
        nextTable = null; // 删除成员变量
        table = nextTab;// 更新 table 数组
        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
    }
}

put 方法第三阶段 - 协助扩容

如果对应的节点存在,判断这个节点的 hash 是不是等于 MOVED(-1),说明当前节点是 ForwardingNode 节点,意味着有其他线程正在进行扩容,那么当前现在直接帮助它进行扩容,因此调用 helpTransfer 方法

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 {
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;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { //在低 16 位上增加扩容线程数
                transfer(tab, nextTab); //帮助扩容
                break;
            }
        }
        return nextTab;
    }
    return table;
}

put 方法第四阶段 -节点已经存在链表,继续添加节点

这个方法的主要作用是,如果被添加的节点的位置已经存在节点的时候,需要以链表的方式加入到节点中

如果当前节点已经是一颗红黑树,那么就会按照红黑树的规则将当前节点加入到红黑树中

else { //进入到这个分支,说明 f 是当前 nodes 数组对应位置节点的头节点,并且不为空
    V oldVal = null;
    synchronized (f) { //给对应的Node()头结点加锁
        if (tabAt(tab, i) == f) {   //再次判断对应下标位置是否为 f 节点
            if (fh >= 0) { //头结点的 hash 值大于 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) { // 如果都找不到相同的key,则继续在链表后追加节点
                        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;
    }
}

put方法第五阶段 是否进行转换红黑树

判断链表的长度是否已经达到临界值 8. 如果达到了临界值,这个时候会根据当前数组的长度 来决定是扩容还是将链表转化为红黑树。也就是说如果当前数组的长度小于 64,就会先扩容。否则,会把当前链表转化为红黑树

static final int TREEIFY_THRESHOLD = 8;

if (binCount != 0) { // 说明上面在做链表操作
    // 如果链表长度已经达到临界值 8 就需要把链表转换为树结构
    if (binCount >= TREEIFY_THRESHOLD)
        treeifyBin(tab, i);
    if (oldVal != null) // 如果 val 是被替换的,则返回替换之前的值
        return oldVal;
    break;
}
treeifyBin

在 putVal 的最后部分,有一个判断,如果链表长度大于 8,那么就会触发扩容或者红黑树的 转化操作。

static final int MIN_TREEIFY_CAPACITY = 64;

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        // tab 的长度是不是小于 64,如果是,则执行扩容
        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));
                }
            }
        }
    }
}
tryPresize

tryPresize 里面部分代码和 addCount 的部分代码类似,看起来会稍微简单一些

private final void tryPresize(int size) {
    //对 size 进 行修复 ,主要目的是防止传入的值不是一个 2 次幂的整数 ,然 后通过tableSizeFor 来讲入参转化为离该整数最近的 2 次幂
    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;
        //下面这段代码和 initTable 是一样的,如果 table 没有初始化,则开始初始化
        if (tab == null || (n = tab.length) == 0) {
            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); //0.75
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        else if (tab == table) { //这段代码和 addCount 后部分代码是一样的,做辅助扩容操作
            int rs = resizeStamp(n);
            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);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}

/**
 * Returns a power of two table size for the given desired capacity.
 * See Hackers Delight, sec 3.2
 */
private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值