JDK1.8并发容器ConcurrentHashMap

基本介绍

ConcurrentHashMap的结构原理图如下,大部分还是跟HashMap的结构类似,TreeBin节点是对于已经树化的红黑树节点的代理节点,FWD节点为标识扩容桶位,它指向扩容之后的nextTableimage.png

常量

ConcurrentHashMap的常量如下:

/* ---------------- Constants -------------- */
/**
 * 散列表数组最大限制
 */
private static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 散列表容量默认值
 */
private static final int DEFAULT_CAPACITY = 16;

/**
 * The largest possible (non-power of two) array size.
 * Needed by toArray and related methods.
 */
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * 并发级别,jdk1.7遗留下来的,1.8只有在初始化的时候用了一用。
 * 不代表并发级别。
 */
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

/**
 * 负载因子,JDK1.8中 ConcurrentHashMap 是固定值
 */
private static final float LOAD_FACTOR = 0.75f;

/**
 * 树化阈值,指定桶位 链表长度达到8的话,有可能发生树化操作。
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 红黑树转化为链表的阈值
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 联合TREEIFY_THRESHOLD控制桶位是否树化,只有当table数组长度达到64且 某个桶位 中的链表长度达到8,才会真正树化
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * 线程迁移数据最小步长,控制线程迁移任务最小区间一个值
 */
private static final int MIN_TRANSFER_STRIDE = 16;

/**
 * 扩容相关,计算扩容时生成的一个标识戳
 */
private static int RESIZE_STAMP_BITS = 16;

/**
 * 65535 表示并发扩容最多线程数
 */
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

/**
 * 扩容相关
 */
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

//当Node节点的hash值为-1时,表示当前节点是FWD节点,已经被迁移了
static final int MOVED     = -1; // hash for forwarding nodes
//当Node节点的hash值为-2时,表示当前节点已经树化,且当前节点为TreeBin对象,TreeBin对象代理操作红黑树
static final int TREEBIN   = -2; // hash for roots of trees
static final int RESERVED  = -3; // hash for transient reservations
//0x7fffffff => 转成二进制位31个1 0111 1111 1111 1111 1111 1111 1111 1111
/**
 * 0111 1111 1111 1111 1111 1111 1111 1111有什么作用呢?
 * 可以将一个负数,即最高符号位为1的数通过与它进行位与运算&得到正数,但不是取绝对值
 */
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

/** Number of CPUS, to place bounds on some sizings.
 * 当前系统的CPU数量
 */
static final int NCPU = Runtime.getRuntime().availableProcessors();

/** For serialization compatibility.
 * JDK1.8序列化为了建容jdk1.7的ConcurrentHashMap保存的
 */
private static final ObjectStreamField[] serialPersistentFields = {
    new ObjectStreamField("segments", Segment[].class),
    new ObjectStreamField("segmentMask", Integer.TYPE),
    new ObjectStreamField("segmentShift", Integer.TYPE)
};
复制代码

成员变量

ConcurrentHashMap的成员变量如下:

/**
 * The array of bins. Lazily initialized upon first insertion.
 * Size is always a power of two. Accessed directly by iterators.
 * 散列表,长度一定是2次方数
 */
transient volatile Node<K,V>[] table;

/**
 * The next table to use; non-null only while resizing.
 * 扩容过程中,会将扩容中的新table 赋值给nextTable 保持引用,扩容结束之后,这里会被设置为Null
 */
private transient volatile Node<K,V>[] nextTable;

/**
 * Base counter value, used mainly when there is no contention,
 * but also as a fallback during table initialization
 * races. Updated via CAS.
 * LongAdder 中的 baseCount 未发生竞争时 或者 当前LongAdder处于加锁状态时,增量累到到baseCount中
 */
private transient volatile long baseCount;

/**
 * sizeCtl < 0
 * 1. -1 表示当前table正在初始化(有线程在创建table数组),当前线程需要自旋等待..
 * 2. 表示当前table数组正在进行扩容 ,高16位表示:扩容的标识戳   低16位表示:(1 + nThread) 当前参与并发扩容的线程数量
 *
 * sizeCtl = 0,表示创建table数组时 使用DEFAULT_CAPACITY为大小
 *
 * sizeCtl > 0
 *
 * 1. 如果table未初始化,表示初始化大小
 * 2. 如果table已经初始化,表示下次扩容时的 触发条件(阈值)
 */
private transient volatile int sizeCtl;

/**
 * 扩容过程中,记录当前进度。所有线程都需要从transferIndex中分配区间任务,去执行自己的任务。
 */
private transient volatile int transferIndex;

/**
 * Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
 * LongAdder中的cellsBuzy 0表示当前LongAdder对象无锁状态,1表示当前LongAdder对象加锁状态
 */
private transient volatile int cellsBusy;

/**
 * Table of counter cells. When non-null, size is a power of 2.
 * LongAdder中的cells数组,当baseCount发生竞争后,会创建cells数组,
 * 线程会通过计算hash值 取到 自己的cell ,将增量累加到指定cell中
 * 总数 = sum(cells) + baseCount
 */
private transient volatile CounterCell[] counterCells;
复制代码
  • table:装载 Node采用懒加载的方式,只有等到第一次插入数据调用put的时候才会进行初始化操作,数组的大小总是为 2 的幂次方。

  • nextTable:扩容过程中,会将扩容中的新table 赋值给nextTable 保持引用,扩容结束之后,这里会被设置为Null。

  • baseCount:在用于计算散列表的元素个数addCount方法中会使用,使用方法跟LongAdder中的base属性一样,当多线程修改baseCount发生竞争时,就会使用上面中的属性counterCells来分散热点,不同的线程被分配到不同的桶位,自己只需要修改对应桶位中的值,最后计算散列表的总元素个数时,将baseCount的值加上数组counterCells的值即为元素个数。关于LongAdder的原理可以查看[LongAdder源码分析]这篇文章,这里就不展开讲了。

  • cellsBusy:该属性的用法跟在LongAdder中的用法一样,LongAdder中的cellsBuzy 0表示当前LongAdder对象无锁状态,1表示当前LongAdder对象加锁状态

  • sizeCtl属性的值的情况如下:

    • -1:表示table数组正在初始化
    • -N:表示当前table数组正在进行扩容,高16位表示扩容的标识戳 低16位表示(1 + nThread) 当前参与并发扩容的线程数量。
    • 等于0:表示创建table数组时使用DEFAULT_CAPACITY即16为大小
    • 大于0:表示如果table未初始化,表示初始化大小,或者如果table已经初始化,表示下次扩容时的 触发条件(阈值)

静态代码块

ConcurrentHashMap的静态代码块如下:

  • Unsafe类通过U.objectFieldOffset(k.getDeclaredField("属性名"));来获取对应属性的偏移地址,方便后面通过Unsafecas修改对应属性值。
  • ABASE = U.arrayBaseOffset(ak);ABASENode数组的头地址
  • int scale = U.arrayIndexScale(ak);:表示数组单元所占用空间大小,scale 表示Node[]数组中每一个单元所占用空间大小。
  • Integer.numberOfLeadingZeros(scale):该方法获取参数二进制表示法中连续0最多的个数,举个简单的例子,假设Node节点所占的空间大小scale4,二进制为100,由于一共有32位,那么100前面还有29位连续的0,所以Integer.numberOfLeadingZeros(4)的值为29
  • ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);:这里为什么要这样计算,有什么用意呢?假设Node节点所占的空间大小scale4,那么ASHIFT = 31 - 29 = 2,比如我想要获取数组中第五个元素的偏移地址 :ABASE + 5 * scale = ABASE + 5 * 4,即第一个元素的起始地址加上元素下标,我们也可以通过ABASE + (5 << ASHIFT)5左移两位,也表示ABASE + 5 * 4,这样获取到对应的偏移地址也是可以,而且这种方式使用位运算,更快。
static {
    try {
        U = sun.misc.Unsafe.getUnsafe();
        Class<?> k = ConcurrentHashMap.class;
        /**
         * 以下都是获取到对应属性的内存偏移量
         */
        SIZECTL = U.objectFieldOffset
            (k.getDeclaredField("sizeCtl"));
        TRANSFERINDEX = U.objectFieldOffset
            (k.getDeclaredField("transferIndex"));
        BASECOUNT = U.objectFieldOffset
            (k.getDeclaredField("baseCount"));
        CELLSBUSY = U.objectFieldOffset
            (k.getDeclaredField("cellsBusy"));
        Class<?> ck = CounterCell.class;
        CELLVALUE = U.objectFieldOffset
            (ck.getDeclaredField("value"));
        Class<?> ak = Node[].class;
        ABASE = U.arrayBaseOffset(ak);
        //表示数组单元所占用空间大小,scale 表示Node[]数组中每一个单元所占用空间大小
        int scale = U.arrayIndexScale(ak);
        //因为scale一定是2的几次幂的,1 0000 & 0 1111 = 0
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        //numberOfLeadingZeros() 这个方法是返回当前数值转换为二进制后,从高位到低位开始统计,看有多少个0连续在一块。
        //8 => 1000 numberOfLeadingZeros(8) = 28
        //4 => 100 numberOfLeadingZeros(4) = 29
        //ASHIFT = 31 - 29 = 2 ??
        //假设scale为4,比如我想要获取数组中第五个元素的偏移地址 :ABASE + 5 * scale
        //ABASE + 5 * scale = ABASE + (5 << ASHIFT)5左移两位,表示5 * 4
        ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
    } catch (Exception e) {
        throw new Error(e);
    }
}
复制代码

内部类

Node

Node节点结构如下,跟HashMap1.8中的类似,不同的是,ConcurrentHashMap中的Node节点中的valnext使用了volatile关键字修饰,表示内存可见性,保证多线程操作的安全。

static class Node<K,V> implements Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;//volatile修饰表示内存可见性
    volatile Node<K,V> next;//volatile修饰表示内存可见性
}
复制代码

TreeNode

TreeNode节点即红黑树节点如下:

/**
 * Nodes for use in TreeBins
 */
static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> parent;  // 父亲节点 
    TreeNode<K,V> left;    //左子节点
    TreeNode<K,V> right;   //右子节点
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;           //默认为红色
}
复制代码

ForwardingNode

ForwardingNode用于标识扩容的桶位,结构如下:

/**
 * A node inserted at head of bins during transfer operations.
 */
static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;//扩容之后新的table数组
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
}
复制代码

当发生扩容时,concurrentHashMap中的ForwardingNode节点原理如下图所示

image.png

当然这里会涉及高低位链表以及高低位双链表(TreeNode)节点的数据迁移过程,会在下面的扩容方法中详细说明。

工具方法

spread(hash)源码分析

spread方法是一个扰动函数,跟HashMap1.8中的hash方法类似,是为了让散列表更加分散,减少hash冲突的。代码如下:

/**
 * 例子:
 * 1100 0011 1010 0101 0001 1100 0001 1110 ==> h
 * 0000 0000 0000 0000 1100 0011 1010 0101 ==> h >>> 16
 * 1100 0011 1010 0101 1101 1111 1011 1011 ==> h ^ (h >>> 16
 * ---------------------------------------
 * 1100 0011 1010 0101 1101 1111 1011 1011 ==> h ^ (h >>> 16
 * 0111 1111 1111 1111 1111 1111 1111 1111 ==> HASH_BITS
 * 0100 0011 1010 0101 1101 1111 1011 1011 ==> (h ^ (h >>> 16)) & HASH_BITS;
 *
 * 看结果的第一位由1变成了0,即&HASH_BITS就是为了得到一个正数
 *
 * 为什么要&HASH_BITS?是为了得到正数的hash值,因为HASH_BITS是31个1
 */
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
复制代码

1.为什么要将key的hashCode右移16位,并且与原来的hashCode进行异或^操作呢?

因为我们知道路由函数为hash & (散列表数组的长度 -1),散列表的长度一般都是2的几次幂,例如假设散列表的长度为16,16-1=15,转为二进制为1111,将hash & 1111最终起作用就是最后的几位,高位起不到作用,所以为了让高位参与运算,才让h ^ (h >>> 16),这样使得散列表出现冲突的次数更少。

2.为什么还要& HASH_BITS

/**
 * 0111 1111 1111 1111 1111 1111 1111 1111有什么作用呢?
 * 可以将一个负数,即最高符号位为1的数通过与它进行位与运算&得到正数,但不是取绝对值
 */
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
复制代码

因为HASH_BITS的值为0x7fffffff,即0111 1111 1111 1111 1111 1111 1111 1111一共31个1,& HASH_BITS是为了让最终的hash值的第一位为0,即变为正数(第一位为1表示负数)

tabAt(tab,index)源码分析

该方法获取对应下标的table桶位元素,至于为什么(long)i << ASHIFT) + ABASE?在上面的静态代码块中解释过,代码如下:

/**
 * 获取table素组中下标位i的node节点
 * 通过(long)i << ASHIFT) + ABASE获取下标i元素的偏移地址
 * ABASE:为table数组的第一个元素的起始地址
 */
@SuppressWarnings("unchecked")
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);
}
复制代码

casTabAt(tab,index,c,v)源码分析

该方法通过cas的方式修改对应下标的table桶位元素,代码如下:

/**
 * 修改table中下标为i的Node元素
 * @param c 期望值
 * @param 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);
}
复制代码

setTabAt(tab,index,v)源码分析

该方法在table对应下标的桶位存入元素,代码如下:

/**
 * 往table数组中下标为i的位置存入Node节点
 */
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
复制代码

resizeStamp(int n)源码分析

在扩容的时候会计算出一个扩容标识戳,因为扩容的操作可能是多个线程一起配合执行的,只有线程拿到的扩容标识戳跟其他的线程一样时,才能协助参与扩容

举个例子:容量从16扩容到32

  • n = 16 = 1000Integer.numberOfLeadingZeros(n) = 27 = 0000 0000 0001 1011
  • RESIZE_STAMP_BITS = 16是一个固定值,1 << (RESIZE_STAMP_BITS - 1) = 1000 0000 0000 0000 = 32768
  • 0000 0000 0001 1011 | 1000 0000 0000 0000 = 1000 0000 0001 1011

即从16扩容到32的扩容标识戳是一个固定值,

代码如下:

/**
 * Returns the stamp bits for resizing a table of size n.
 * Must be negative when shifted left by RESIZE_STAMP_SHIFT.
 * 在扩容的时候会计算出一个扩容标识戳,
 * 因为扩容的操作可能是多个线程一起配合执行的,每个线程拿到的扩容表标识戳是一样的
 * 16 -> 32 从16扩容到32
 * numberOfLeadingZeros(16) => 1 0000 =>27 =>0000 0000 0001 1011
 * |
 * (1 << (RESIZE_STAMP_BITS - 1)) => 1000 0000 0000 0000 => 32768
 * ---------------------------------------------------------------
 * 0000 0000 0001 1011
 * 1000 0000 0000 0000
 * 1000 0000 0001 1011
 *
 * Integer.numberOfLeadingZeros(n):获取到n的32位中连续0的个数最多个数,比如16 => 10000,最多为前面的27个连续0
 * RESIZE_STAMP_BITS是一个固定值16
 */
static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
复制代码

tableSizeFor(int c)方法

该方法在JDK1.8中HashMap中的是一样的效果,是为了获得返回>=c的最小的2的次方数。 代码如下:

/**
 * Returns a power of two table size for the given desired capacity.
 * See Hackers Delight, sec 3.2
 * 返回>=c的最小的2的次方数
 * c=28
 * n=27 => 0b 11011
 * 11011 | 01101 => 11111
 * 11111 | 00111 => 11111
 * ....
 * => 11111 + 1 =100000 = 32
 */
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;
}
复制代码

构造方法

常用的构造方法如下:

/**
 * Creates a new, empty map with the default initial table size (16).
 * 无参构造函数
 */
public ConcurrentHashMap() {
}

public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();

    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
                //tableSizeFor会返回一个大于传进参数的最小2次幂
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    /**
     * sizeCtl > 0
     * 当目前table未初始化时,sizeCtl表示初始化容量
     */
    this.sizeCtl = cap;
}

/**
 * this(initialCapacity, loadFactor, 1)会调用下面的构造方法
 */
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}

/**
 * 在ConcurrentHashMap中负载因子是一个用final修饰的,o.75f
 * concurrencyLevel没什么实际的意义
 */
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();

    if (initialCapacity < concurrencyLevel)   // Use at least as many bins
        initialCapacity = concurrencyLevel;   // as estimated threads

    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    /**
     * sizeCtl > 0
     * 当目前table未初始化时,sizeCtl表示初始化容量
     */
    this.sizeCtl = cap;
}
复制代码

核心方法写操作put(k,v)

putVal()方法

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

/** Implementation for put and putIfAbsent */
//onlyIfAbsent:如果传进去的key存在,则将value的值覆盖,如果为true,则不覆盖
final V putVal(K key, V value, boolean onlyIfAbsent) {
    //控制k 和 v 不能为null
    if (key == null || value == null) throw new NullPointerException();

    //通过spread方法,可以让高位也能参与进寻址运算。
    int hash = spread(key.hashCode());
    //binCount表示当前k-v 封装成node后插入到指定桶位后,在桶位中的所属链表的下标位置
    //0 表示当前桶位为null,node可以直接放着
    //2 表示当前桶位已经可能是红黑树
    int binCount = 0;

    //tab 引用map对象的table
    //自旋
    for (Node<K,V>[] tab = table;;) {
        //f 表示桶位的头结点
        //n 表示散列表数组的长度
        //i 表示key通过寻址计算后,得到的桶位下标
        //fh 表示桶位头结点的hash值
        Node<K,V> f; int n, i, fh;

        //CASE1:成立,表示当前map中的table尚未初始化..
        if (tab == null || (n = tab.length) == 0)
            //最终当前线程都会获取到最新的map.table引用。
            tab = initTable();
        //CASE2:i 表示key使用路由寻址算法得到 key对应 table数组的下标位置,tabAt 获取指定桶位的头结点 f
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //进入到CASE2代码块 前置条件 当前table数组i桶位是Null时。
            //使用CAS方式 设置 指定数组i桶位 为 new Node<K,V>(hash, key, value, null),并且期望值是null
            //cas操作成功 表示ok,直接break for循环即可
            //cas操作失败,表示在当前线程之前,有其它线程先你一步向指定i桶位设置值了。
            //当前线程只能再次自旋,去走其它逻辑。
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }

        //CASE3:前置条件,桶位的头结点一定不是null。
        //条件成立表示当前桶位的头结点 为 FWD结点,表示目前map正处于扩容过程中..
        else if ((fh = f.hash) == MOVED)
            //看到fwd节点后,当前节点有义务帮助当前map对象完成迁移数据的工作
            //学完扩容后再来看。
            tab = helpTransfer(tab, f);

        //CASE4:当前桶位 可能是 链表 也可能是 红黑树代理结点TreeBin
        else {
            //当插入key存在时,会将旧值赋值给oldVal,返回给put方法调用处..
            V oldVal = null;

            //使用sync 加锁“头节点”,理论上是“头结点”
            synchronized (f) {
                //为什么又要对比一下,看看当前桶位的头节点 是否为 之前获取的头结点?
                //为了避免其它线程将该桶位的头结点修改掉,导致当前线程从sync 加锁 就有问题了。之后所有操作都不用在做了。
                if (tabAt(tab, i) == f) {//条件成立,说明咱们 加锁 的对象没有问题,可以进来造了!

                    //条件成立,说明当前桶位就是普通链表桶位。
                    if (fh >= 0) {
                        //1.当前插入key与链表当中所有元素的key都不一致时,当前的插入操作是追加到链表的末尾,binCount表示链表长度
                        //2.当前插入key与链表当中的某个元素的key一致时,当前插入操作可能就是替换了。binCount表示冲突位置(binCount - 1)
                        binCount = 1;

                        //迭代循环当前桶位的链表,e是每次循环处理节点。
                        for (Node<K,V> e = f;; ++binCount) {
                            //当前循环节点 key
                            K ek;
                            //条件一:e.hash == hash 成立 表示循环的当前元素的hash值与插入节点的hash值一致,需要进一步判断
                            //条件二:((ek = e.key) == key ||(ek != null && key.equals(ek)))
                            //       成立:说明循环的当前节点与插入节点的key一致,发生冲突了
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                //将当前循环的元素的 值 赋值给oldVal
                                oldVal = e.val;

                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            //当前元素 与 插入元素的key不一致 时,会走下面程序。
                            //1.更新循环处理节点为 当前节点的下一个节点
                            //2.判断下一个节点是否为null,如果是null,说明当前节点已经是队尾了,插入数据需要追加到队尾节点的后面。

                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    //前置条件,该桶位一定不是链表
                    //条件成立,表示当前桶位是 红黑树代理结点TreeBin
                    else if (f instanceof TreeBin) {
                        //p 表示红黑树中如果与你插入节点的key 有冲突节点的话 ,则putTreeVal 方法 会返回冲突节点的引用。
                        Node<K,V> p;
                        //强制设置binCount为2,因为binCount <= 1 时有其它含义,所以这里设置为了2 回头讲 addCount。
                        binCount = 2;

                        //条件一:成立,说明当前插入节点的key与红黑树中的某个节点的key一致,冲突了
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            //将冲突节点的值 赋值给 oldVal
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }

            //说明当前桶位不为null,可能是红黑树 也可能是链表
            if (binCount != 0) {
                //如果binCount>=8 表示处理的桶位一定是链表
                if (binCount >= TREEIFY_THRESHOLD)
                    //调用转化链表为红黑树的方法
                    treeifyBin(tab, i);
                //说明当前线程插入的数据key,与原有k-v发生冲突,需要将原数据v返回给调用者。
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }

    //1.统计当前table一共有多少数据
    //2.判断是否达到扩容阈值标准,触发扩容。
    addCount(1L, binCount);

    return null;
}
复制代码
  • 1.首先通过spread(key.hashCode())计算出对应的hash值,接着判断table数组是否初始化过,如果没有初始化,则调用initTable进行初始化。

  • 2.然后(n - 1) & hash)计算出当前hash值对应的table桶位下标,如果该桶位为空,则创建Node节点通过casTabAt(底层通过Unsafe类的cas)将Node节点填充到该桶位上,完成操作。

  • 3.如果(n - 1) & hash)对应的table桶位不为空,则接着判断该桶位元素的fh = f.hash) == MOVED,即判断hash值是否等于-1,如果等于-1表示该桶位当前为FWD节点,表示目前map正处于扩容过程中,接着调用helpTransfer去协助其他线程一起完成扩容。

  • 4.如果步骤3条件不成立的话,说明当前桶位可能是链表,也可能是 红黑树代理结点TreeBin,接着使用synchronized独占锁的方式锁住当前桶位节点,保证只能有一个线程对该桶位的所有元素进行修改,接着继续判断当前桶位的hash值如果大于0,说明当前桶位就是普通链表桶位,接着遍历单向链表,如果找到链表中的节点与待插入的hash值一样并且key一样或者调用equals方法返回为true的话,就进行覆盖操作,否则将带插入节点插入到单向链表的末尾。如果当前桶位f instanceof TreeBinTreeBin节点的话,就调用putTreeVal方法进行插入节点(该方法在下面的TreeBin中会详细讲解)。

  • 5.完成节点插入操作之后,判断binCount是否大于TREEIFY_THRESHOLD,即是否大于8,如果大于8,则将单向链表调用treeifyBin树化

  • 6.最后调用addCount方法统计当前table一共有多少数据 散列表一共有多少数据,判断是否达到扩容阈值标准,触发扩容。

initTable()方法

初始化table数组的源码如下:

/**
 * Initializes table, using the size recorded in sizeCtl.
 * 该方法可能会被多个线程调用
 *      * sizeCtl < 0
 *      * 1. -1 表示当前table正在初始化(有线程在创建table数组),当前线程需要自旋等待..
 *      * 2.表示当前table数组正在进行扩容 ,高16位表示:扩容的标识戳   低16位表示:(1 + nThread) 当前参与并发扩容的线程数量
 *      *
 *      * sizeCtl = 0,表示创建table数组时 使用DEFAULT_CAPACITY为大小
 *      *
 *      * sizeCtl > 0
 *      *
 *      * 1. 如果table未初始化,表示初始化大小
 *      * 2. 如果table已经初始化,表示下次扩容时的 触发条件(阈值)
 */
private final Node<K,V>[] initTable() {
    //tab 引用map.table
    //sc sizeCtl的临时值
    Node<K,V>[] tab; int sc;
    //自旋 条件:map.table 尚未初始化
    while ((tab = table) == null || tab.length == 0) {

        if ((sc = sizeCtl) < 0)
            //大概率就是-1,表示其它线程正在进行创建table的过程,当前线程没有竞争到初始化table的锁。
            //Thread.yield();让当前线程释放CPU运行权限
            Thread.yield(); // lost initialization race; just spin

        //1.sizeCtl = 0,表示创建table数组时 使用DEFAULT_CAPACITY为大小
        //2.如果table未初始化,表示初始化大小
        //3.如果table已经初始化,表示下次扩容时的 触发条件(阈值)
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                //这里为什么又要判断呢? 防止其它线程已经初始化完毕了,然后当前线程再次初始化..导致丢失数据。
                //条件成立,说明其它线程都没有进入过这个if块,当前线程就是具备初始化table权利了。
                if ((tab = table) == null || tab.length == 0) {

                    //sc大于0 创建table时 使用 sc为指定大小,否则使用 16 默认值.
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;

                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    //最终赋值给 map.table
                    table = tab = nt;
                    //n >>> 2  => 等于 1/4 n     n - (1/4)n = 3/4 n => 0.75 * n
                    //sc 0.75 n 表示下一次扩容时的触发条件。
                    sc = n - (n >>> 2);
                }
            } finally {
                //1.如果当前线程是第一次创建map.table的线程话,sc表示的是 下一次扩容的阈值
                //2.表示当前线程 并不是第一次创建map.table的线程,当前线程进入到else if 块 时,将
                //sizeCtl 设置为了-1 ,那么这时需要将其修改为 进入时的值。
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
复制代码

核心方法统计addCount()方法

addCount方法主要有两个作用

  • 统计当前table一共有多少数据 散列表一共有多少数据
  • 判断是否达到扩容阈值标准,触发扩容。因为除了插入元素putVal方法会调用,删除元素的remove方法也会调用。
/**
 * Adds to count, and if table is too small and not already
 * resizing, initiates transfer. If already resizing, helps
 * perform transfer if work is available.  Rechecks occupancy
 * after a transfer to see if another resize is already needed
 * because resizings are lagging additions.
 *
 * @param x the count to add
 * @param check if <0, don't check resize, if <= 1 only check if uncontended
 *
 * 1.统计当前table一共有多少数据 散列表一共有多少数据
 * 2.判断是否达到扩容阈值标准,触发扩容。
 */
private final void addCount(long x, int check) {
    //as 表示 LongAdder.cells
    //b 表示LongAdder.base
    //s 表示当前map.table中元素的数量
    CounterCell[] as; long b, s;
    //条件一:true->表示cells已经初始化了,当前线程应该去使用hash寻址找到合适的cell 去累加数据
    //       false->表示当前线程应该将数据累加到 base
    //条件二:false->表示写base成功,数据累加到base中了,当前竞争不激烈,不需要创建cells
    //       true->表示写base失败,与其他线程在base上发生了竞争,当前线程应该去尝试创建cells。
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        //有几种情况进入到if块中?
        //1.true->表示cells已经初始化了,当前线程应该去使用hash寻址找到合适的cell 去累加数据
        //2.true->表示写base失败,与其他线程在base上发生了竞争,当前线程应该去尝试创建cells。

        //a 表示当前线程hash寻址命中的cell
        CounterCell a;
        //v 表示当前线程写cell时的期望值
        long v;
        //m 表示当前cells数组的长度
        int m;
        //true -> 未竞争  false->发生竞争
        boolean uncontended = true;


        //条件一:as == null || (m = as.length - 1) < 0
        //true-> 表示当前线程是通过 写base竞争失败 然后进入的if块,就需要调用fullAddCount方法去扩容 或者 重试.. LongAdder.longAccumulate
        //条件二:a = as[ThreadLocalRandom.getProbe() & m]) == null   前置条件:cells已经初始化了
        //true->表示当前线程命中的cell表格是个空,需要当前线程进入fullAddCount方法去初始化 cell,放入当前位置.
        //条件三:!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)
        //      false->取反得到false,表示当前线程使用cas方式更新当前命中的cell成功
        //      true->取反得到true,表示当前线程使用cas方式更新当前命中的cell失败,需要进入fullAddCount进行重试 或者 扩容 cells。
        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就是LongAdder中的longAccumulate方法
            fullAddCount(x, uncontended);
            //考虑到fullAddCount里面的事情比较累,就让当前线程 不参与到 扩容相关的逻辑了,直接返回到调用点。
            return;
        }

        if (check <= 1)
            return;

        //获取当前散列表元素个数,这是一个期望值
        //就是LongAdder中的sum方法
        s = sumCount();
    }

    /**
     * check >= 1 表示插入元素对应的桶位链表的长度
     * check == 0 表示插入的元素key对应的桶位为空,即直接插入到桶位中,这是check为0
     * check == 2 表示插入的元素key对应的桶位已经树化了
     *
     * check < 0 表示不是调用put插入元素方法进来的addCount,而是remove方法进来的addCount,也就不会进入下面的if块中
     */
    //表示一定是一个put操作调用的addCount
    if (check >= 0) {
        //tab 表示map.table
        //nt 表示map.nextTable
        //n 表示map.table数组的长度
        //sc 表示sizeCtl的临时值
        Node<K,V>[] tab, nt; int n, sc;


        /**
         * sizeCtl < 0
         * 1. -1 表示当前table正在初始化(有线程在创建table数组),当前线程需要自旋等待..
         * 2.表示当前table数组正在进行扩容 ,高16位表示:扩容的标识戳   低16位表示:(1 + nThread) 当前参与并发扩容的线程数量
         *
         * sizeCtl = 0,表示创建table数组时 使用DEFAULT_CAPACITY为大小
         *
         * sizeCtl > 0
         *
         * 1. 如果table未初始化,表示初始化大小
         * 2. 如果table已经初始化,表示下次扩容时的 触发条件(阈值)
         */

        //自旋
        //条件一:s >= (long)(sc = sizeCtl)
        //       true-> 1.当前sizeCtl为一个负数 表示正在扩容中..,当前线程应该去协助完成扩容
        //              2.当前sizeCtl是一个正数,表示扩容阈值
        //       false-> 表示当前table尚未达到扩容条件
        //条件二:(tab = table) != null
        //       恒成立 true
        //条件三:(n = tab.length) < MAXIMUM_CAPACITY
        //       true->当前table长度小于最大值限制,则可以进行扩容。
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {

            //扩容批次唯一标识戳
            //16 -> 32 扩容 标识为:1000 0000 0001 1011 即只要是从16扩容到32拿到的扩容标识戳都是一样的
            int rs = resizeStamp(n);

            //条件成立:表示当前table正在扩容
            //         当前线程理论上应该协助table完成扩容
            if (sc < 0) {
                //条件一:(sc >>> RESIZE_STAMP_SHIFT) != rs 将下面的1000 0000 0001 1011 0000 0000 0000 0010只保留高16,即为原来的扩容标识戳
                //      true->说明当前线程获取到的扩容唯一标识戳 非 本批次扩容
                //      false->说明当前线程获取到的扩容唯一标识戳 是 本批次扩容
                //条件二: JDK1.8 中有bug jira已经提出来了 其实想表达的是 =  sc == (rs << 16 ) + 1
                //        true-> 表示扩容完毕,当前线程不需要再参与进来了
                //        false->扩容还在进行中,当前线程可以参与
                //条件三:JDK1.8 中有bug jira已经提出来了 其实想表达的是 = sc == (rs<<16) + MAX_RESIZERS
                //        true-> 表示当前参与并发扩容的线程达到了最大值 65535 - 1
                //        false->表示当前线程可以参与进来
                //条件四:(nt = nextTable) == null
                //        true->表示本次扩容结束
                //        false->扩容正在进行中
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;

                //前置条件:当前table正在执行扩容中.. 当前线程有机会参与进扩容。
                //条件成立:说明当前线程成功参与到扩容任务中,并且将sc低16位值加1,表示多了一个线程参与工作,即(1 + nThread)
                //条件失败:1.当前有很多线程都在此处尝试修改sizeCtl,有其它一个线程修改成功了,导致你的sc期望值与内存中的值不一致 修改失败
                //        2.transfer 任务内部的线程也修改了sizeCtl。
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    //协助扩容线程,持有nextTable参数
                    transfer(tab, nt);
            }

            //RESIZE_STAMP_SHIFT为16,将扩容标识戳左移16位再加2
            //1000 0000 0001 1011 0000 0000 0000 0000 +2 => 1000 0000 0001 1011 0000 0000 0000 0010
            // 1000 0000 0001 1011 0000 0000 0000 0010 高16位还是扩容标识戳不变,低16位为(1 + nThread) 1+1=2
            //条件成立,说明当前线程是触发扩容的第一个线程,将sizeCtl修改为负数了,因为第一位符号位为1,在transfer方法需要做一些扩容准备工作
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                //触发扩容条件的线程 不持有nextTable
                transfer(tab, null);
            s = sumCount();
        }
    }
}
复制代码
  • 1.使用跟LongAdder一样的方法最终调用sumCount方法计算出当前散列表中的元素总个数,当然底层也是通过将baseCount加上CounterCell数组中的元素值求出的总和。

  • 2.接着判断addCount方法的第二个参数check是否大于0,在调用插入元素putVal方法内部调用addCount方法传入的check肯定是大于0的,而删除元素remove方法内部调用addCount方法传入的check的值小于0.

  • 3.如果check大于0,接着在while循环中调用resizeStamp计算出扩容唯一标识戳,接着判断属性sizeCtl此时的值是否小于0,我们在上面知道,sizeCtl的值小于0表示当前table正在扩容,当前线程理论上应该协助table完成扩容,然后通过cas将sizeCtl的值加1,为什么要加1呢?因为第一个线程在扩容时,会将sizeCtl的值修改为扩容标识戳 << RESIZE_STAMP_SHIFT) + 2,即扩容标识戳左移16位,即高16位就是扩容标识戳,低16位为0,再让低1位加上22表示当前有1 + n个线程在扩容,即只有一个线程在扩容的时候,低16位的值为2,由于此时最高位的值为1,符号位为1表示负数,所以经过第一个扩容线程将sizeCtl的值修改之后,sizeCtl的值变为了小于0。回到上面为什么加1?应该很好理解了吧?就是将sizeCtl的低16位表示的扩容线程数量增加1呗。然后调用transfer(tab, nt)去协助其他线程扩容。

  • 如果第一次进来sizeCtl的值不小于0的话,则说明当前线程是参与扩容的第一个线程,接着按照上面步骤3说的将sizeCtl的值修改为扩容标识戳 << RESIZE_STAMP_SHIFT) + 2,最后调用transfer方法去扩容。

超级核心方法扩容transfer方法

扩容的流程图如下:

image.png

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    //n 表示扩容之前table数组的长度
    //stride 表示分配给线程任务的步长,规定一个线程将长度为stride的桶位数量进行数据迁移
    int n = tab.length, stride;
    //方便讲解源码  stride 固定为 16
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range


    //条件成立:表示当前线程为触发本次扩容的线程,需要做一些扩容准备工作
    //条件不成立:表示当前线程是协助扩容的线程..
    if (nextTab == null) {            // initiating
        try {
            //创建了一个比扩容之前大一倍的table
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        //赋值给对象属性 nextTable ,方便协助扩容线程 拿到新表
        nextTable = nextTab;
        //记录迁移数据整体位置的一个标记。index计数是从1开始计算的。
        transferIndex = n;
    }

    //表示新数组的长度
    int nextn = nextTab.length;
    //fwd 节点,当某个桶位数据处理完毕后,即该桶位的所有数据迁移完毕了,将此桶位设置为fwd节点,其它写线程 或读线程看到后,会有不同逻辑。
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    //推进标记
    boolean advance = true;
    //完成标记
    boolean finishing = false; // to ensure sweep before committing nextTab

    //i 表示分配给当前线程任务,执行到的桶位
    //bound 表示分配给当前线程任务的下界限制
    int i = 0, bound = 0;
    //自旋
    for (;;) {
        //f 桶位的头结点
        //fh 头结点的hash
        Node<K,V> f; int fh;


        /**
         * 1.给当前线程分配任务区间
         * 2.维护当前线程任务进度(i 表示当前处理的桶位)
         * 3.维护map对象全局范围内的进度
         */
        while (advance) {
            //分配任务的开始下标
            //分配任务的结束下标
            int nextIndex, nextBound;

            //CASE1:
            //条件一:--i >= bound
            //成立:表示当前线程的任务尚未完成,还有相应的区间的桶位要处理,--i 就让当前线程处理下一个 桶位.
            //不成立:表示当前线程任务已完成 或 者未分配
            if (--i >= bound || finishing)
                advance = false;
            //CASE2:
            //前置条件:当前线程任务已完成 或 者未分配
            //条件成立:表示对象全局范围内的桶位都分配完毕了,没有区间可分配了,设置当前线程的i变量为-1 跳出循环后,执行退出迁移任务相关的程序
            //条件不成立:表示对象全局范围内的桶位尚未分配完毕,还有区间可分配
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            //CASE3:
            //前置条件:1、当前线程需要分配任务区间  2.全局范围内还有桶位尚未迁移
            //条件成立:说明给当前线程分配任务成功
            //条件失败:说明分配给当前线程失败,应该是和其它线程发生了竞争吧
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {

                //当前线程处理的桶位下界值
                bound = nextBound;
                //当前线程处理的桶位上界值 从下往上处理
                i = nextIndex - 1;
                advance = false;
            }
        }

        //CASE1:
        //条件一:i < 0,即i=-1时
        //成立:表示当前线程未分配到任务
        if (i < 0 || i >= n || i + n >= nextn) {
            //保存sizeCtl 的变量
            int sc;
            if (finishing) {
                //最后一个扩容线程退出之前
                nextTable = null;
                table = nextTab;
                // sizeCtl = 0.75的2n
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }

            //条件成立:说明设置sizeCtl ,因为低16表示有(1 + nThread)个线程在扩容 ,低16位, -1 成功,当前线程可以正常退出
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                //1000 0000 0001 1011 0000 0000 0000 0000
                //条件成立:说明当前线程不是最后一个退出transfer任务的线程
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    //正常退出
                    return;

                //最后一个扩容线程退出之前需要做一些检查工作,怕有一些遗漏的桶中数据还没有迁移,进行迁移
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        //前置条件:【CASE2~CASE4】 当前线程任务尚未处理完,正在进行中

        //CASE2:
        //条件成立:说明当前桶位未存放数据,只需要将此处设置为fwd节点即可。
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        //CASE3:
        //条件成立:说明当前桶位已经迁移过了,当前线程不用再处理了,直接再次更新当前线程任务索引,再次处理下一个桶位 或者 其它操作
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        //CASE4:
        //前置条件:当前桶位有数据,而且node节点 不是 fwd节点,说明这些数据需要迁移。
        else {
            //sync 加锁当前桶位的头结点
            synchronized (f) {
                //防止在你加锁头对象之前,当前桶位的头对象被其它写线程修改过,导致你目前加锁对象错误...
                if (tabAt(tab, i) == f) {
                    //ln 表示低位链表引用
                    //hn 表示高位链表引用
                    Node<K,V> ln, hn;

                    //条件成立:表示当前桶位是链表桶位
                    if (fh >= 0) {


                        //lastRun
                        //可以获取出 当前链表 末尾连续高位不变的 node
                        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;
                            }
                        }

                        //条件成立:说明lastRun引用的链表为 低位链表,那么就让 ln 指向 低位链表
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        //否则,说明lastRun引用的链表为 高位链表,就让 hn 指向 高位链表
                        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;
                    }
                    //条件成立:表示当前桶位是 红黑树 代理结点TreeBin
                    else if (f instanceof TreeBin) {
                        //转换头结点为 treeBin引用 t
                        TreeBin<K,V> t = (TreeBin<K,V>)f;

                        //低位双向链表 lo 指向低位链表的头  loTail 指向低位链表的尾巴
                        TreeNode<K,V> lo = null, loTail = null;
                        //高位双向链表 lo 指向高位链表的头  loTail 指向高位链表的尾巴
                        TreeNode<K,V> hi = null, hiTail = null;


                        //lc 表示低位链表元素数量
                        //hc 表示高位链表元素数量
                        int lc = 0, hc = 0;

                        //迭代TreeBin中的双向链表,从头结点 至 尾节点
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            // h 表示循环处理当前元素的 hash
                            int h = e.hash;
                            //使用当前节点 构建出来的 新的 TreeNode
                            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;
                                //将低位链表尾指针指向 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;
                    }
                }
            }
        }
    }
}
复制代码
  • 1.首先如果判断传进来的nextTab是否为空,如果为空则说明当前线程是第一个扩容线程,需要做一些扩容之前的准备工作:
    • 创建比之前table数组大一倍的新数组Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]
    • 将新的table数组赋值给属性nextTable
    • 将旧的table数组的长度n赋值给transferIndex,用于下面当前记录迁移数据整体位置的一个标记。index计数是从1开始计算的。
  • 2.接着在for自旋里面的while循环里面,给当前线程分配数据迁移的桶位区间,从boundi,但是下面处理数据迁移的时候是从下往上处理的,即先处理i,接着i-1,i-2...直到bound。
  • 3.接着判断如果i已经小于0的话,说明当前线程没有分配到数据迁移区间,接着通过cas修改sizeCtl的低16位的值减一,相信大家还记得,这里的减一不就是因为当前线程没有任务处理了,退出,扩容的线程数就要减一了呗,然后接着判断(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT,其中RESIZE_STAMP_SHIFT的值为16,为什么要这样判断呢?不就是为了判断当前线程是否是最后一个扩容线程嘛!因为我们知道扩容的时候sizeCtl的低16位表示当前参与扩容的线程数+1.如果当前线程不是最后一个扩容线程,则直接退出扩容方法,否则还需要做一些额外的处理,比如:
    • 检查是否所有的桶位的数据都已经迁移完毕了,如果还有尚未迁移的桶位数据则进行迁移
    • 所有桶位数据都迁移完毕之后,接着将新数组赋值给属性table,属性nextTable值置为空。
    • sizeCtl的值设置为(n << 1) - (n >>> 1),即下一次扩容的阈值为0.75的2n.最后退出扩容方法。
  • 4.接着下面的逻辑就是针对任务区间从i开始到bound进行数据迁移,首先判断当前处理的桶位是否为空,如果为空的话,直接通过casTabAt(tab, i, null, fwd)将当前桶位设置为fwd节点即可
  • 5.如果当前桶位元素的hash值为-1,则表示是FWD节点,当前桶位已经迁移过了,当前线程不用再处理了。
  • 6.如果当前桶位不为空,并且当前桶位不是FWD节点的话,说明当前桶位可能是链表,也可能是TreeBin节点。接着使用synchronized (f)锁住当前桶位节点,然后判断当前桶位元素的hash值是否大于0,如果大于0则表示当前桶位是链表头节点,则使用高低位链表将节点串起来,存入到新的散列表中。
  • 7.如果当前桶位节点是TreeBin节点,则同样使用高低位双向链表将TreeNode节点串起来,调用new TreeBin<K,V>(lo)TreeBin的使用双向链表构造红黑树方法构造红黑树,最终存入到新的散列表中。

扩容的数据迁移工作是由多个线程一起合作完成的,简单的理解如下图所示

image.png

高低位链表迁移原理如下图所示

image.png 将当前节点的hash值跟原table的长度n进行&操作,如果(h & n) == 0说明该节点的hash值的高位为0,扩容之后存放的桶位下标不变,反之为1扩容之后的下标为i+n,对应高位链表。s

核心方法之helpTransfer()协助扩容方法

协助扩容的方法如下:

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    //nextTab 引用的是 fwd.nextTable == map.nextTable 理论上是这样。
    //sc 保存map.sizeCtl
    Node<K,V>[] nextTab; int sc;

    //条件一:tab != null 恒成立 true
    //条件二:(f instanceof ForwardingNode) 恒成立 true
    //条件三:((ForwardingNode<K,V>)f).nextTable) != null 恒成立 true
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {

        //拿当前标的长度 获取 扩容标识戳   假设 16 -> 32 扩容:1000 0000 0001 1011
        int rs = resizeStamp(tab.length);

        //条件一:nextTab == nextTable
        //成立:表示当前扩容正在进行中
        //不成立:1.nextTable被设置为Null 了,扩容完毕后,会被设为Null
        //       2.再次出发扩容了...咱们拿到的nextTab 也已经过期了...
        //条件二:table == tab
        //成立:说明 扩容正在进行中,还未完成
        //不成立:说明扩容已经结束了,扩容结束之后,最后退出的线程 会设置 nextTable 为 table

        //条件三:(sc = sizeCtl) < 0
        //成立:说明扩容正在进行中
        //不成立:说明sizeCtl当前是一个大于0的数,此时代表下次扩容的阈值,当前扩容已经结束。
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {


            //条件一:(sc >>> RESIZE_STAMP_SHIFT) != rs
            //      true->说明当前线程获取到的扩容唯一标识戳 非 本批次扩容
            //      false->说明当前线程获取到的扩容唯一标识戳 是 本批次扩容
            //条件二: JDK1.8 中有bug jira已经提出来了 其实想表达的是 =  sc == (rs << 16 ) + 1
            //        true-> 表示扩容完毕,当前线程不需要再参与进来了
            //        false->扩容还在进行中,当前线程可以参与
            //条件三:JDK1.8 中有bug jira已经提出来了 其实想表达的是 = sc == (rs<<16) + MAX_RESIZERS
            //        true-> 表示当前参与并发扩容的线程达到了最大值 65535 - 1
            //        false->表示当前线程可以参与进来
            //条件四:transferIndex <= 0
            //      true->说明map对象全局范围内的任务已经分配完了,当前线程进去也没活干..
            //      false->还有任务可以分配。
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;

            
            //更新sizeCtl的低16位,当前参与扩容的线程数量+1
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}
复制代码

get方法

根据key获取value方法如下:

public V get(Object key) {
    //tab 引用map.table
    //e 当前元素
    //p 目标节点
    //n table数组长度
    //eh 当前元素hash
    //ek 当前元素key
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //扰动运算后得到 更散列的hash值
    int h = spread(key.hashCode());

    //条件一:(tab = table) != null
    //true->表示已经put过数据,并且map内部的table也已经初始化完毕
    //false->表示创建完map后,并没有put过数据,map内部的table是延迟初始化的,只有第一次写数据时会触发创建逻辑。
    //条件二:(n = tab.length) > 0 true->表示table已经初始化
    //条件三:(e = tabAt(tab, (n - 1) & h)) != null
    //true->当前key寻址的桶位 有值
    //false->当前key寻址的桶位中是null,是null直接返回null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        //前置条件:当前桶位有数据

        //对比头结点hash与查询key的hash是否一致
        //条件成立:说明头结点与查询Key的hash值 完全一致
        if ((eh = e.hash) == h) {
            //完全比对 查询key 和 头结点的key
            //条件成立:说明头结点就是查询数据
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }

        //条件成立:
        //1.-1  fwd 说明当前table正在扩容,且当前查询的这个桶位的数据 已经被迁移走了
        //2.-2  TreeBin节点,需要使用TreeBin 提供的find 方法查询。
        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;
}
复制代码

image.png

在FWD中find方法实现如下:

/**
 * 在迁移数据后查找元素
 */
Node<K,V> find(int h, Object k) {
    // loop to avoid arbitrarily deep recursion on forwarding nodes
    //tab 一定不为空
    Node<K,V>[] tab = nextTable;
    outer: for (;;) {
        //n 表示为扩容而创建的 新表的长度
        //e 表示在扩容而创建新表使用 寻址算法 得到的 桶位头结点
        Node<K,V> e; int n;

        //条件一:永远不成立
        //条件二:永远不成立
        //条件三:永远不成立
        //条件四:在新扩容表中 重新定位 hash 对应的头结点
        //true -> 1.在oldTable中 对应的桶位在迁移之前就是null
        //        2.扩容完成后,有其它写线程,将此桶位设置为了null
        if (k == null || tab == null || (n = tab.length) == 0 ||
            (e = tabAt(tab, (n - 1) & h)) == null)
            return null;

        //前置条件:扩容后的表 对应hash的桶位一定不是null,e为此桶位的头结点
        //e可能为哪些node类型?
        //1.node 类型
        //2.TreeBin 类型
        //3.FWD 类型

        for (;;) {
            //eh 新扩容后表指定桶位的当前节点的hash
            //ek 新扩容后表指定桶位的当前节点的key
            int eh; K ek;
            //条件成立:说明新扩容 后的表,当前命中桶位中的数据,即为 查询想要数据。
            if ((eh = e.hash) == h &&
                ((ek = e.key) == k || (ek != null && k.equals(ek))))
                return e;

            //eh<0
            //1.TreeBin 类型    2.FWD类型(新扩容的表,在并发很大的情况下,可能在此方法 再次拿到FWD类型..)
            if (eh < 0) {
                if (e instanceof ForwardingNode) {
                    tab = ((ForwardingNode<K,V>)e).nextTable;
                    continue outer;
                }
                else
                    //说明此桶位 为 TreeBin 节点,使用TreeBin.find 查找红黑树中相应节点。
                    return e.find(h, k);
            }

            //前置条件:当前桶位头结点 并没有命中查询,说明此桶位是 链表
            //1.将当前元素 指向链表的下一个元素
            //2.判断当前元素的下一个位置 是否为空
            //   true->说明迭代到链表末尾,未找到对应的数据,返回Null
            if ((e = e.next) == null)
                return null;
        }
    }
}
复制代码

remove方法

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


final V replaceNode(Object key, V value, Object cv) {
    //计算key经过扰动运算后的hash
    int hash = spread(key.hashCode());
    //自旋
    for (Node<K,V>[] tab = table;;) {
        //f表示桶位头结点
        //n表示当前table数组长度
        //i表示hash命中桶位下标
        //fh表示桶位头结点 hash
        Node<K,V> f; int n, i, fh;

        //CASE1:
        //条件一:tab == null  true->表示当前map.table尚未初始化..  false->已经初始化
        //条件二:(n = tab.length) == 0  true->表示当前map.table尚未初始化..  false->已经初始化
        //条件三:(f = tabAt(tab, i = (n - 1) & hash)) == null true -> 表示命中桶位中为null,直接break, 会返回
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;

        //CASE2:
        //前置条件CASE2 ~ CASE3:当前桶位不是null
        //条件成立:说明当前table正在扩容中,当前是个写操作,所以当前线程需要协助table完成扩容。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);

        //CASE3:
        //前置条件CASE2 ~ CASE3:当前桶位不是null
        //当前桶位 可能是 "链表" 也可能 是  "红黑树" TreeBin
        else {
            //保留替换之前的数据引用
            V oldVal = null;
            //校验标记
            boolean validated = false;
            //加锁当前桶位 头结点,加锁成功之后会进入 代码块。
            synchronized (f) {
                //判断sync加锁是否为当前桶位 头节点,防止其它线程,在当前线程加锁成功之前,修改过 桶位 的头结点。
                //条件成立:当前桶位头结点 仍然为f,其它线程没修改过。
                if (tabAt(tab, i) == f) {
                    //条件成立:说明桶位 为 链表 或者 单个 node
                    if (fh >= 0) {
                        validated = true;

                        //e 表示当前循环处理元素
                        //pred 表示当前循环节点的上一个节点
                        Node<K,V> e = f, pred = null;
                        for (;;) {
                            //当前节点key
                            K ek;
                            //条件一:e.hash == hash true->说明当前节点的hash与查找节点hash一致
                            //条件二:((ek = e.key) == key || (ek != null && key.equals(ek)))
                            //if 条件成立,说明key 与查询的key完全一致。
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                //当前节点的value
                                V ev = e.val;

                                //条件一:cv == null true->替换的值为null 那么就是一个删除操作
                                //条件二:cv == ev || (ev != null && cv.equals(ev))  那么是一个替换操作
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
                                    //删除 或者 替换

                                    //将当前节点的值 赋值给 oldVal 后续返回会用到
                                    oldVal = ev;

                                    //条件成立:说明当前是一个替换操作
                                    if (value != null)
                                        //直接替换
                                        e.val = value;
                                    //条件成立:说明当前节点非头结点
                                    else if (pred != null)
                                        //当前节点的上一个节点,指向当前节点的下一个节点。
                                        pred.next = e.next;

                                    else
                                        //说明当前节点即为 头结点,只需要将 桶位设置为头结点的下一个节点。
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == null)
                                break;
                        }
                    }

                    //条件成立:TreeBin节点。
                    else if (f instanceof TreeBin) {
                        validated = true;

                        //转换为实际类型 TreeBin t
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        //r 表示 红黑树 根节点
                        //p 表示 红黑树中查找到对应key 一致的node
                        TreeNode<K,V> r, p;

                        //条件一:(r = t.root) != null 理论上是成立
                        //条件二:TreeNode.findTreeNode 以当前节点为入口,向下查找key(包括本身节点)
                        //      true->说明查找到相应key 对应的node节点。会赋值给p
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            //保存p.val 到pv
                            V pv = p.val;

                            //条件一:cv == null  成立:不必对value,就做替换或者删除操作
                            //条件二:cv == pv ||(pv != null && cv.equals(pv)) 成立:说明“对比值”与当前p节点的值 一致
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
                                //替换或者删除操作


                                oldVal = pv;

                                //条件成立:替换操作
                                if (value != null)
                                    p.val = value;


                                //删除操作
                                else if (t.removeTreeNode(p))
                                    //这里没做判断,直接搞了...很疑惑
                                    //将双向链表转为单向链表
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            //当其他线程修改过桶位 头结点时,当前线程 sync 头结点 锁错对象时,validated 为false,会进入下次for 自旋
            if (validated) {

                if (oldVal != null) {
                    //替换的值 为null,说明当前是一次删除操作,oldVal !=null 成立,说明删除成功,更新当前元素个数计数器。
                    if (value == null)
                        //check为-1
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}
复制代码

TreeBin源码

TreeBin结构如下:

static final class TreeBin<K,V> extends Node<K,V> {
    //红黑树 根节点 小刘讲师录制的红黑树教程:av83540396
    TreeNode<K,V> root;
    //双向链表的头节点
    volatile TreeNode<K,V> first;
    //等待者线程(当前lockState是读锁状态)
    volatile Thread waiter;
    /**
     * 1.写锁状态 写是独占状态,以散列表来看,真正进入到TreeBin中的写线程 同一时刻 只有一个线程。 1
     * 2.读锁状态 读锁是共享,同一时刻可以有多个线程 同时进入到 TreeBin对象中获取数据。 每一个线程 都会给 lockState + 4
     * 3.等待者状态(写线程在等待),当TreeBin中有读线程目前正在读取数据时,写线程无法修改数据,那么就将lockState的最低2位 设置为 0b 10
     */
    volatile int lockState;

    // values for lockState
    static final int WRITER = 1; // set while holding write lock
    static final int WAITER = 2; // set when waiting for write lock
    static final int READER = 4; // increment value for setting read lock
}
复制代码

lockState属性的值分三种状态,如下:

  • 1.写锁状态 写是独占状态,以散列表来看,真正进入到TreeBin中的写线程 同一时刻 只有一个线程,值为1。
  • 2.读锁状态 读锁是共享,同一时刻可以有多个线程 同时进入到 TreeBin对象中获取数据。 每一个线程 都会给 lockState + 4
  • 3.等待者状态(写线程在等待),当TreeBin中有读线程目前正在读取数据时,写线程无法修改数据,那么就将lockState的最低2位 设置为 0b 10

使用TreeNode中的双向链表构建红黑树,代码如下:

/**
 * Creates bin with initial set of nodes headed by b.
 * 构建红黑树,b为双向链表的头元素
 *
 * 用TreeNode双向链表构建红黑树
 */
TreeBin(TreeNode<K,V> b) {
    //设置节点hash为-2 表示此节点是TreeBin节点
    super(TREEBIN, null, null, null);
    //使用first 引用 treeNode链表
    this.first = b;
    //r 红黑树的根节点引用
    TreeNode<K,V> r = null;

    //x表示遍历的当前节点
    for (TreeNode<K,V> x = b, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        //强制设置当前插入节点的左右子树为null
        x.left = x.right = null;
        //条件成立:说明当前红黑树 是一个空树,那么设置插入元素 为根节点
        if (r == null) {
            //根节点的父节点 一定为 null
            x.parent = null;
            //颜色改为黑色
            x.red = false;
            //让r引用x所指向的对象。
            r = x;
        }

        else {
            //非第一次循环,都会来带else分支,此时红黑树已经有数据了

            //k 表示 插入节点的key
            K k = x.key;
            //h 表示 插入节点的hash
            int h = x.hash;
            //kc 表示 插入节点key的class类型
            Class<?> kc = null;
            //p 表示 为查找插入节点的父节点的一个临时节点
            TreeNode<K,V> p = r;

            for (;;) {
                //dir (-1, 1)
                //-1 表示插入节点的hash值大于 当前p节点的hash
                //1 表示插入节点的hash值 小于 当前p节点的hash
                //ph p表示 为查找插入节点的父节点的一个临时节点的hash
                int dir, ph;
                //临时节点 key
                K pk = p.key;

                //插入节点的hash值 小于 当前节点
                if ((ph = p.hash) > h)
                    //插入节点可能需要插入到当前节点的左子节点 或者 继续在左子树上查找
                    dir = -1;
                //插入节点的hash值 大于 当前节点
                else if (ph < h)
                    //插入节点可能需要插入到当前节点的右子节点 或者 继续在右子树上查找
                    dir = 1;

                //如果执行到 CASE3,说明当前插入节点的hash 与 当前节点的hash一致,会在case3 做出最终排序。最终
                //拿到的dir 一定不是0,(-1, 1)
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                //xp 想要表示的是 插入节点的 父节点
                TreeNode<K,V> xp = p;
                //条件成立:说明当前p节点 即为插入节点的父节点
                //条件不成立:说明p节点 底下还有层次,需要将p指向 p的左子节点 或者 右子节点,表示继续向下搜索。
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    //设置插入节点的父节点 为 当前节点
                    x.parent = xp;
                    //小于P节点,需要插入到P节点的左子节点
                    if (dir <= 0)
                        xp.left = x;

                        //大于P节点,需要插入到P节点的右子节点
                    else
                        xp.right = x;

                    //插入节点后,红黑树性质 可能会被破坏,所以需要调用 平衡方法
                    r = balanceInsertion(r, x);
                    break;
                }
            }
        }
    }
    //将r 赋值给 TreeBin对象的 root引用。
    this.root = r;
    assert checkInvariants(root);
}
复制代码

最终TreeBin的结构如下图所示

image.png

在上面的get方法中除了可能会调用FWDfind方法,还可能会调用TreeBinfind方法,如下:

final Node<K,V> find(int h, Object k) {
    if (k != null) {

        //e 表示循环迭代的当前节点   迭代的是first引用的链表
        for (Node<K,V> e = first; e != null; ) {
            //s 保存的是lock临时状态
            //ek 链表当前节点 的key
            int s; K ek;


            //(WAITER|WRITER) => 0010 | 0001 => 0011
            //lockState & 0011 != 0 条件成立:说明当前TreeBin 有等待者线程 或者 目前有写操作线程正在加锁
            //如果红黑树被加锁了,不阻塞,就查询双向链表,秒阿,这就是双向链表的作用
            if (((s = lockState) & (WAITER|WRITER)) != 0) {
                if (e.hash == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
                e = e.next;
            }

            //前置条件:当前TreeBin中 等待者线程 或者 写线程 都没有
            //条件成立:说明添加读锁成功
            else if (U.compareAndSwapInt(this, LOCKSTATE, s,
                                         s + READER)) {
                TreeNode<K,V> r, p;
                try {
                    //查询红黑树操作
                    p = ((r = root) == null ? null :
                         r.findTreeNode(h, k, null));
                } finally {
                    //w 表示等待者线程
                    Thread w;
                    //U.getAndAddInt(this, LOCKSTATE, -READER) == (READER|WAITER)
                    //1.当前线程查询红黑树结束,释放当前线程的读锁 就是让 lockstate 值 - 4
                    //(READER|WAITER) = 0110 => 表示当前只有一个线程在读,且“有一个线程在等待”
                    //当前读线程为 TreeBin中的最后一个读线程。

                    //2.(w = waiter) != null 说明有一个写线程在等待读操作全部结束。即调用了LockSupport.park方法。
                    if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
                        (READER|WAITER) && (w = waiter) != null)
                        //使用unpark 让 写线程 恢复运行状态。
                        LockSupport.unpark(w);
                }
                return p;
            }
        }
    }
    return null;
}
复制代码
  • 在上面的find方法中,如果当前TreeBin中的红黑树有等待线程(写线程在等待)或者加了写锁的话,那就只能从双向链表中读取数据,否则直接从红黑树中读取数据。

  • 否则通过cas修改lockState的值为lockState + 4,即表示加了一个读锁,然后从红黑树root节点出发,比较hash值查找对应节点。

  • 通过第二种方法查询到节点之后,将lockState - 4,接着判断当前是否是只有写线程在等待,如果是的话,就调用LockSupport.unpark(w);唤醒写线程

往红黑树中插入节点putTreeVal方法,如果节点的key与待插入的key一样的话,返回该节点

/**
 * Finds or adds a node.
 * @return null if added
 * 往红黑树中插入节点,如果节点的key与待插入的key一样的话,返回该节点
 */
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        if (p == null) {
            first = root = new TreeNode<K,V>(h, k, v, null, null);
            break;
        }
        else if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
            return p;
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                     (q = ch.findTreeNode(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.findTreeNode(h, k, kc)) != null))
                    return q;
            }
            dir = tieBreakOrder(k, pk);
        }


        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            //当前循环节点xp 即为 x 节点的爸爸

            //x 表示插入节点
            //f 老的头结点
            TreeNode<K,V> x, f = first;
            //双向链表插入为头插法,新插入的节点指向老节点
            first = x = new TreeNode<K,V>(h, k, v, f, xp);

            //条件成立:说明链表有数据
            if (f != null)
                //设置老的头结点的前置引用为 当前的头结点。
                f.prev = x;


            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;


            if (!xp.red)
                x.red = true;
            else {
                //表示 当前新插入节点后,新插入节点 与 父节点 形成 “红红相连”
                //锁住红黑树
                lockRoot();
                try {
                    //平衡红黑树,使其再次符合规范。
                    root = balanceInsertion(root, x);
                } finally {
                    //释放红黑树
                    unlockRoot();
                }
            }
            break;
        }
    }
    assert checkInvariants(root);
    return null;
}

/**
 * Acquires write lock for tree restructuring.
 */
private final void lockRoot() {
    //条件成立:说明lockState 并不是 0,说明此时有其它读线程在treeBin红黑树中读取数据。
    if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
        contendedLock(); // offload to separate method
}

/**
 * Releases write lock for tree restructuring.
 */
private final void unlockRoot() {
    lockState = 0;
}

/**
 * Possibly blocks awaiting root lock.
 * 目前有读线程在读取红黑树
 */
private final void contendedLock() {
    boolean waiting = false;
    //表示lock值
    int s;
    //自旋
    for (;;) {
        //~WAITER = 11111....01
        //条件成立:说明目前TreeBin中没有读线程在访问 红黑树
        //条件不成立:有线程在访问红黑树
        if (((s = lockState) & ~WAITER) == 0) {
            //条件成立:说明写线程 抢占锁成功
            if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
                if (waiting)
                    //设置TreeBin对象waiter 引用为null
                    waiter = null;
                return;
            }
        }
        //lock & 0000...10 = 0, 条件成立:说明lock 中 waiter 标志位 为0,此时当前线程可以设置为1了,然后将当前线程挂起。
        else if ((s & WAITER) == 0) {
            if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
                waiting = true;
                waiter = Thread.currentThread();
            }
        }
        //条件成立:说明当前线程在CASE2中已经将 treeBin.waiter 设置为了当前线程,并且将lockState 中表示 等待者标记位的地方 设置为了1
        //这个时候,就让当前线程 挂起。。
        else if (waiting)
            LockSupport.park(this);
    }
}
复制代码

在往红黑树中插入元素之后,在调用红黑树平衡方法balanceInsertion之前,需要调用lockRoot通过cas的方式获取写锁(通过修改属性lockState的值来控制),获取成功的话就进行平衡调整,最后释放锁,否则调用LockSupport.park(this)进行挂起。

猜你喜欢

转载自juejin.im/post/7036724850357960712