java源码分析之-ConcurrentHashMap1.8

ConcurrentHashMap源码分析

最近在看 ConcurrentHashMap 1.8版本的源码,有了些自己的见解,想与大家分享一番,理解有不对的地方还请大家指正。

ConcurrentHashMap 源码应该怎么读,大家可以先在脑海里想一下,ConcurrentHashMap 在自己的项目中比较常用的几个方法,
然后以这些方法作为入口,顺着往下一条线进行阅读,接着发散到其他关联的一下方法逻辑,最后由点到面。

大家应该都知道ConcurrentHashMap 1.8相对于1.7的 数组+链表(hash冲突时形成链表,头插法)结构,调整成
数组+链表(hash冲突时形成链表,尾插法)+红黑树(链表节点数>=8时转成红黑树) 结构。
而1.7时期Segment分段加锁结构也被去掉,不过虽然Segment被去掉,但仍然保留了块的概念, 只是1.8中的分段其实就是table数组中一个个的hash槽,这样使得添加节点时加锁粒度更小,并发度也更高
至于为何在链表节点达到一定程度后转成红黑树的原因,也是因为获取数据时如果链表长度太长,那么最差情况下迭代时间复杂度达到O(n),效率相对低下,而红黑树的查找复杂度只有Olog(n)。
总之,1.8引入的各种结构目的:在线程安全的情况下,能有更高的并发度和查询效率,即大约能保持有HashMap的性能。

ConcurrentHashMap1.8的数据结构图一览

红黑树动态演示可以点击此网址查看
在这里插入图片描述

ConcurrentHashMap1.8主要函数一览

在这里插入图片描述

源码解读

构造函数

ConcurrentHashMap构造函数可以指定初始容量initialCapacity,和加载因子loadFactor 等属性,不过这里要注意,
如果构造时指定initialCapacity,最终构造出的ConcurrentHashMap初始容量不一定就是initialCapacity,而是一个大于或等于 initialCapacity 的2的幂次方(2^n)的数。

/************************************1.构造函数*******************************************/
public ConcurrentHashMap() {}

/**
 * @param initialCapacity 初始容量
 */
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
            MAXIMUM_CAPACITY :
            tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

/**
 * @param initialCapacity 初始容量
 * @param loadFactor 加载因子
 */
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}

/**
 * @param initialCapacity 初始容量
 * @param loadFactor 加载因子
 * @param 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);
    this.sizeCtl = cap;
}

/**
 *
 * @param m 初始化map的数据
 */
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(m);
}

不知道大家有没注意到,当构造CHM时如果指定了加载因子loadFactor,只会在初始化时生效,后续的容量增长还是使用默认的 0.75;这里可能也是一个考点哦。

ConcurrentHashMap一些重要属性

/**
 * The array of bins. Lazily initialized upon first insertion.
 * Size is always a power of two. Accessed directly by iterators.<br/>
 *
 * 根据注释知道,table就是存放一个个散列hash槽的基础数组结构。
 * 它的长度必须是2的n次方,且数组是第一个元素插入的时候才进行初始化。
 */
transient volatile Node<K,V>[] table;
/**
 * The next table to use; non-null only while resizing.<br/>
 * nextTable,只在 ConcurrentHashMap 发生 resize 扩容的时候不为空
 */
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.<br/>
 * baseCount: 存放map存放的节点个数,仅在没有线程竞争的情况下使用
 * (map的节点数目发生变化发生在添加,删除操作中)
 */
private transient volatile long baseCount;
/**
 * Table initialization and resizing control.  When negative, the
 * table is being initialized or resized: -1 for initialization,
 * else -(1 + the number of active resizing threads).  Otherwise,
 * when table is null, holds the initial table size to use upon
 * creation, or 0 for default. After initialization, holds the
 * next element count value upon which to resize the table.<br/>
 *
 * sizeCtl : 表初始化时的容量和触发扩容操作的阈值。
 * 分以下几种情况:
 *  负数:
 *      -1 : table[] 数组正在初始化中
 *      很大的负数:存放的是扩容时,第一个执行扩容线程的标识(一个基于扩容前容量n的随机数[版本号]),
 *      以及当前正在参与map扩容操作的线程数(原文注释中仅仅说该值标识的是当前参与resizeing的线程数,其实不太准确)
 *      这里我们先保留一个疑问: sizeCtl是如何保存发生扩容时第一个线程的标识的?(提示:高低位)
 *
 *  正数:
 *      当table[]为null时,保存的是进行table初始化时的长度(可能是 0 -当调用的是默认无参构造函数时)
 *      当table[]初始化完毕,则存放的是下一次触发扩容的容量阈值
 */
private transient volatile int sizeCtl;
/**
 * The next table index (plus one) to split while resizing.
 * 扩容时,用以标识下一个操作节点转移的索引(table.index + 1)
 */
private transient volatile int transferIndex;
/**
 * Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
 * 当发生扩容时,用以创建 counterCells 数组的自旋锁标识
 */
private transient volatile int cellsBusy;

/**
 * Table of counter cells. When non-null, size is a power of 2.
 * 当发生线程竞争添加添加map时,counterCells 不为空,保存的是每一个线程添加节点的数量。
 */
private transient volatile CounterCell[] counterCells;

ConcurrentHashMap一些常量

/**
 * The largest possible table capacity.  This value must be
 * exactly 1<<30 to stay within Java array allocation and indexing
 * bounds for power of two table sizes, and is further required
 * because the top two bits of 32bit hash fields are used for
 * control purposes.
 *
 * table最大容量值,最高位是一个符号控制位
 */
private static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 初始table[]容量值
 */
private static final int DEFAULT_CAPACITY = 16;

/**
 * 数组最大长度
 */
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * 默认最大的并发级别
 */
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

/**
 * 默认加载因子0.75
 */
private static final float LOAD_FACTOR = 0.75f;

/**
 * hash槽上链表节点数最少达到8个时,转成红黑树,其实是9个才会转成树结构
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 红黑树回退到链表结构的节点临界值 6个
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * The value should be at least 4 * TREEIFY_THRESHOLD to avoid
 * conflicts between resizing and treeification thresholds.
 *
 * 最小的转变为红黑树结构的节点数目,即当table的长度 >= 64后,且单hash槽内的链表长度大于8个时,才会形成红黑树结构
 *  < 64时只进行resize扩容,此时hash槽内不会有红黑树结构
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * Minimum number of rebinnings per transfer step. Ranges are
 * subdivided to allow multiple resizer threads.  This value
 * serves as a lower bound to avoid resizers encountering
 * excessive memory contention.  The value should be at least
 * DEFAULT_CAPACITY.
 *
 * 扩容线程每次负责转移的hash槽的最小个数
 * (即发生扩容时,一个线程一次只能分配 DEFAULT_CAPACITY=16个hash槽。因为CHM允许多个线程同时参与扩容操作,因此
 * 为了减少线程之间的竞争,设定每个线程一次能处理的hash槽数量,达到线程间分段隔离以减少线程竞争的目的。
 * 如table程度为64,则最多同时允许 64/DEFAULT_CAPACITY = 4 个线程对CHM进行扩容)
 */
private static final int MIN_TRANSFER_STRIDE = 16;

/**
 * The number of bits used for generation stamp in sizeCtl.
 * Must be at least 6 for 32bit arrays.
 * 扩容时生成时间戳(令牌)的偏移量
 * 【这里使用的具体场景是:假设数组长度为n,发生扩容时 从 n -> 2n ;
 * 此时需要使用一个令牌来标识本次扩容操作。该令牌生成会根据n和 RESIZE_STAMP_BITS
 * 做一系列运算之后得出,每次扩容都是唯一的】
 */
private static int RESIZE_STAMP_BITS = 16;

/**
 * The maximum number of threads that can help resize.
 * Must fit in 32 - RESIZE_STAMP_BITS bits.
 * 每次扩容时,可以支持的最大并发线程数,这里的值取 (1 << 16) - 1
 * (为何最大只能是 2^16 -1 个,后续会讲解,提示: 与 sizeCtl 字段在扩容时存的值有关系
 * [sizeCtl 在扩容时同时保存了令牌(版本号)和线程数])
 */
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

/**
 * The bit shift for recording size stamp in sizeCtl.
 * 扩容时令牌(版本号)偏移数
 */
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

/*
 * Node节点hash值,标识为指定的特殊节点,正常的Node节点hash值就是 key.hashCode >0
 */
static final int MOVED     = -1; // ForwardingNode 节点hash标识
static final int TREEBIN   = -2; // TreeBin红黑树头节点hash标识
static final int RESERVED  = -3; // ReservedNode 节点标识
static final int HASH_BITS = 0x7fffffff; // 正常节点hash散列的可用位数,hashCode返回的是一个int值,最大也就是32位。

/** 可用CPU核数 */
static final int NCPU = Runtime.getRuntime().availableProcessors();

一些Unfase方法和属性

private static final sun.misc.Unsafe U;

private static final long SIZECTL;//sizeCtl在 ConcurrentHashMap 中的偏移地址
private static final long TRANSFERINDEX;//transferIndex在 ConcurrentHashMap 中的偏移地址
private static final long BASECOUNT;//baseCount在 ConcurrentHashMap 中的偏移地址
private static final long CELLSBUSY;//cellsBusy ConcurrentHashMap 中的偏移地址
private static final long CELLVALUE;
private static final long ABASE;//获取数组中第一个元素的起始偏移量
private static final int ASHIFT;//返回的是一个数组上节点占用大小所表示的进位数,如一个元素的大小为4,则ASHIFT=2 (4需要二进制进位2次才能达到)

static {
    try {
        /**
         * 获取字节对象中非静态方法的偏移量 objectFieldOffset()
         *
         * */
        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 = ConcurrentHashMap.CounterCell.class;
        CELLVALUE = U.objectFieldOffset
                (ck.getDeclaredField("value"));
        Class<?> ak = ConcurrentHashMap.Node[].class;
        /*
        获取数组中第一个元素的偏移量

        这里返回的起始偏移量是16,因为数组再内存中分配时,头部会额外开辟空间保存数组的长度(int类型64位),
        疑问点:
            为啥是16(解答:Java分配对象时,对象头(32位为例)信息一般为 Mark Word(32 bits)  |  Klass Word (32 bits);
            如果是数组元素,对象头信息还要增加一个  array length (32 bits);

            64位系统时,Mark Word(64 bits) | Klass Word (64 bits) | Padding(?) 填充,其余与32位一致;
            正常来说应该是 64 +64 + 32 = 160,因为jvm默认开启了指针类型压缩,所以 Klass 为32bits,因此这里的 ABASE为 64 + 32 + 32 = 128bits,
            计算机中内存地址一般为十六进制的值 0x3456,计算机中存储器的容量是以字节为基本单位的。
            也就是说一个内存地址代表一个字节(8bit)的存储空间,因此数组元素中对象头的内存长度为 128bits/8 = 16字节)

            类指针压缩: -XX:+UseCompressedClassPointers  这个使得 Klass Pointer 从64位变成 32位
            普通对象指针压缩:-XX:+UseCompressedOops  (Oops : ordinary object pointer)
        */
        ABASE = U.arrayBaseOffset(ak);
        //获取数组中一个元素的大小,这里返回4,这个不知道为什么是4,是因为每个数据节点其实是个Node引用,一个引用相当于一个指针,32位的压缩指针?
        int scale = U.arrayIndexScale(ak);
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);//
    } catch (Exception e) {
        throw new Error(e);
    }
}

节点Node数据结构

CHM内每一个新增的节点都是一个Node,当同一hash槽table[index] 上hash冲突时,形成Node链表结构

/*
* CHM内部关键结构Node,该结构就是存储每一个<key,value>对的节点
* */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;//hey的散列值,这里不一定是 key.hashCode,只要是一个可以唯一标识key的散列值即可
    final K key;
    volatile V val;
    volatile Node<K,V> next;//next,hash冲突时链表连接指针

    Node(int hash, K key, V val, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }

    public final K getKey()       { return key; }
    public final V getValue()     { return val; }
    public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
    public final String toString(){ return key + "=" + val; }
    public final V setValue(V value) {
        throw new UnsupportedOperationException();
    }

    Node<K,V> find(int h, Object k) {
        Node<K,V> e = this;
        if (k != null) {
            do {
                K ek;
                if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
            } while ((e = e.next) != null);
        }
        return null;
    }
}

红黑树节点TreeNode数据结构

/**
 * Nodes for use in TreeBins
 * TreeNode节点继承自Node,因此也拥有了Node上的特性
 */
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;// 节点的前驱节点,用于删除当前节点时,使用 pred.next = curNode.next
    boolean red;//红 黑标识

    /**
     * 从构造函数可以看出,TreeNode节点还保留了Node节点时的特性,
     * 即如果是链表进化到树结构时,树节点还保留了当初链表的连接结构node.next
     */
    TreeNode(int hash, K key, V val, Node<K,V> next,
             TreeNode<K,V> parent) {
        super(hash, key, val, next);
        this.parent = parent;
    }

    Node<K,V> find(int h, Object k) {
        return findTreeNode(h, k, null);
    }

    /**
     * 节点查找
     */
    final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
        if (k != null) {
            TreeNode<K,V> p = this;
            do  {
                int ph, dir; K pk; TreeNode<K,V> q;
                TreeNode<K,V> pl = p.left, pr = p.right;
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                    return p;
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                else if ((kc != null ||
                        (kc = comparableClassFor(k)) != null) &&
                        (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.findTreeNode(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
        }
        return null;
    }
}

红黑树占位节点TreeBin数据结构

/**
 * TreeNodes used at the heads of bins. TreeBins do not hold user
 * keys or values, but instead point to list of TreeNodes and
 * their root. They also maintain a parasitic read-write lock
 * forcing writers (who hold bin lock) to wait for readers (who do
 * not) to complete before tree restructuring operations.
 */
static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K, V> root;
    volatile TreeNode<K, V> first;
    volatile Thread waiter;//当前等待获取锁的线程
    volatile int lockState;
    /*
    * 锁状态的值 二进制低3位分别表示:
    * 低第1位:写锁
    * 低第2位:等待获取写锁
    * 低第3位:读取锁(读锁可叠加,每加一把读锁增加一个 READER)
    * */
    static final int WRITER = 1; // 写锁状态值 0000 0001
    static final int WAITER = 2; // 等待写锁状态值 0000 0010
    static final int READER = 4; // 读锁 0000 0100
}

从作者大段的注释中我们可以知道TreeBin有以下特点:
1.TreeBin自身不保存实际的数据key-value
2.TreeBin内部保存了红黑树的头节点root
3.TreeBin维护了一个读写锁(采用lockState字段保存),而且任何写操作(持有bin锁的线程,其实就是添加了节点)必须等待读取线程读取完成后才能进行树结构的调整;
再通俗点说,就是树的写入,导致红黑树需要重新平衡,此时如果有其他线程在读取树的节点,则必须等待读取完成后,才能进行红黑树平衡调整。

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;//发生扩容时,保存扩容时的新数组
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }

    Node<K,V> find(int h, Object k) {
        // loop to avoid arbitrarily deep recursion on forwarding nodes
        outer: for (Node<K,V>[] tab = nextTable;;) {//节点查找时是在nextTable上查找
            Node<K,V> e; int n;
            if (k == null || tab == null || (n = tab.length) == 0 ||
                (e = tabAt(tab, (n - 1) & h)) == null)
                return null;
            for (;;) {
                int eh; K ek;
                if ((eh = e.hash) == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
                if (eh < 0) {
                    if (e instanceof ForwardingNode) {
                        tab = ((ForwardingNode<K,V>)e).nextTable;
                        continue outer;
                    }
                    else
                        return e.find(h, k);
                }
                if ((e = e.next) == null)
                    return null;
            }
        }
    }
}

根据上面的代码我们可以知道:
ForwardingNode 节点只会在扩容时出现,且是一个标记节点,当原始数组上hash槽被标记为ForwardingNode 节点时,所有的查找操作都是进入到 nextTable查找,因此扩容时ForwardingNode 节点的构造必须设置nextTable。

ReservationNode保留节点

/**
 * A place-holder node used in computeIfAbsent and compute
 */
static final class ReservationNode<K,V> extends Node<K,V> {
    ReservationNode() {
        super(RESERVED, null, null, null);
    }

    Node<K,V> find(int h, Object k) {
        return null;
    }
}

没有实际使用到,用于map.computeIfAbsent 聚合操作时的节点标记

hash数组节点操作

/**
 * 获取table数组第i个位置的数据
 * @param tab 数组
 * @param i 数组下标
 * @param <K>
 * @param <V>
 * @return
 */
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    /**
    * 查找key的hashcode & (n - 1) 对应的数组下标下的node节点,数组每个元素Node占用4个字节,
    * 因此,获取数组内第 i个元素,其偏移量则为 4*i+基准偏移(第一个元素的起始偏移)
    * ABASE:Node[]数组中第一个元素的起始偏移量
    * ASHIFT:Node元素大小的二进制进位数,如元素大小为4,则进位数为 2
    * */
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

/**
 *替换tab数组下指定下标i的元素值 compareAndSwap操作
 */
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);
}

/**
 * 直接设置tab数组下指定下标i的元素值
 */
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

get()查找节点

/*
	查找key对应的值
	读操作没有加锁原因:
		1.添加节点时,如果是链表结构,添加的节点会放置在链表的尾部,而查找时是从链表头部开始,不影响链表的循环
		2.如果是红黑树的结构,当红黑树正在调整时,使用的是较慢的方式:链表迭代进行查找节点,而不是等待树调整后再查找;如果再循环的过程中,红黑树已经调整完毕,
		则又会自动采用红黑树查找方式进行遍历
		3.如果是ForwardNode,则会进入nextTab进行查找,查找方式同样是链表或红黑树查找方式进行遍历
*/
public V get(Object key) {
	Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
	//获取扰动后的hashcode
	int h = spread(key.hashCode());
	if ((tab = table) != null && (n = tab.length) > 0 &&
		(e = tabAt(tab, (n - 1) & h)) != null) {//tabAt(tab, (n - 1) & h)获取tab[i]上的节点链表
		if ((eh = e.hash) == h) {
			if ((ek = e.key) == key || (ek != null && key.equals(ek)))
				return e.val;
		}
		else if (eh < 0) //hash存的值<0,则可能是ForwardingNode:-1,TreeBin:-2;
			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;
}

关于红黑树结构时节点查找时如何保证能获取到的问题,可以查看红黑树节点查找

put()添加节点

在进行CHM添加节点put方法源码阅读之前,我们可以先思考几个问题:
1.添加节点时如何保证添加成功

2.如何保证线程安全

3.当一个节点key刚添加成功,此时触发了扩容,而正好又有一个线程调用get(key)想要获取key的值,此时能获取到嘛?
如果能获取到,map是怎么保证正在扩容时也能查询到数据的?

下面,我们顺着put方法阅读代码

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

/**
 * 用于 put 和 putIfAbsent 方法,
 * @param key
 * @param value
 * @param onlyIfAbsent 仅在key不存在时添加
 * @return
 */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    /*
    * 根据key的hashCode重新获取到一个hash,来作为待插入节点Node(hash,key,val,null)的唯一标识,
    * 为何不直接使用key的hashCode呢?带着问题,我们先来看看 spread()函数
    * */
    int hash = spread(key.hashCode());
    int binCount = 0;//记录节点数的字段
    /*
     * 下边的操作就是:找到key要放置的hash槽,放入节点,返回原始的value值,如果key已存在。
     * 大家想想这里为什么使用循环?原因有多种:
     *  1.table数组未初始化,此时是第一个节点的插入,先进行数组初始化(延迟初始化,可能基于内存资源的考虑)
     *  2.map正在扩容,此时不允许添加节点,自旋尝试直到扩容完毕,添加节点(这里解释下:当线程1添加的节点发现所在的table[i]正好是ForwardingNode节点,说明此时map正在扩容,且当前节点正在被rehash转移中,此时table[i]是不可以添加节点的,需要重新循环检测,直到table[i]转移完毕,也就是map扩容完毕后才能添加节点,而其他的table[j,k....]地方,如果正在被其他线程操作添加,此时还被没标记为ForwardingNode节点时,还是可以直接添加节点的)
     *  3.添加节点时由于其他原因,失败了,重新尝试添加,保证节点添加成功
     * */
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;//fh:节点f的hash
        if (tab == null || (n = tab.length) == 0) //分支1
            tab = initTable();//数组为空,则进行map的 hash数组初始化
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//分支2 
            /*原子操作,设置tab[i]位置首节点,不需要加锁,正常只会有一个线程能设置成功,
            其余执行失败的线程会继续走for循环,之后再也不会进这一个分支
            */
            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)//分支3
            /*
            * fh.hash=MOVED=-1 说明hash槽 table[i]内节点为一个 ForwardingNode,
            * table数组中该 hash槽 table[i] 内的元素已经被rehash并转移到了扩容时的临时数组nextTable中,
            * 且此时数组正在扩容还未完成(扩容完成后table[]上不会存在 ForwardingNode 节点),
            * 这个时候肯定是不可以直接添加节点的,必须要自旋等到扩容完成以后,才能再尝试添加节点(注意:其他槽table[k]未被标记为ForwardingNode的,其他线程还是可以执行put操作)。
            * Doug Lea大神想到,与其让线程自旋空等着,浪费资源,不如你也来帮我扩容吧,
            * 所以就有了帮助扩容helpTransfer()方法
            * */
            tab = helpTransfer(tab, f);
        else {//分支4
            //以上条件都不满足,则table[i]上的节点不为空,准备执行节点添加操作
            V oldVal = null;
            /*
            * 使用 synchronized 关键字对table[i]内是头节点进行加锁,
            * 这里保证了多个线程往同一个table[i]添加节点时串行线程安全操作
            */
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {//分支4-1
                        //fh >= 0 说明是一个链表,如果key已经存在就要看是否要覆盖原来的值,否则一直循环到链表尾节点,将新增的节点放到放到尾部
                        binCount = 1;//binCount 在链表结构时表示的是当前链表的节点数(含即将添加的那个新的节点)
                        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;
                            /*
                            * 找到尾节点后,将新加的节点添加到尾部,这里可以看出采用的是尾插法,一定程度上保证新增加的节点可以立刻被其他线程获取,
                            * 假定 链表长度为 5
                            * 线程1添加操作:刚执行了put(key,val),且刚执行到pred.next = newNode
                            * 线程2获取操作:刚刚遍历到链表第5个元素,判断完是否为我要找的节点,再次进入 next ==null ? 判断,此时发现不为空,元素正好是新加的节点
                            *
                            * 这里为何可以立即能获取到新增的节点呢(Node.next 被 volatile修饰,保证了新增后立即可以获取到)
                            * */
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                        value, null);
                                break;
                            }
                        }
                    }
                    /*
                    * fh <0 ,此时可能为 -2:TreeBin 或者 -3:RESERVED,需要判断
                    * */
                    else if (f instanceof TreeBin) {//分支4-2
                        //fh=-2,说明是一个红黑树
                        Node<K,V> p;
                        binCount = 2;//binCount红黑树时为什么要设置位2 ?大神的随性而为?
                        /*
                            往红黑树中插入一个节点,这里可能会有key相等时的情况,所以putTreeVal时如果发现key相等,
                            则返回key相等的旧节点,目的是为了做后续的值覆盖操作(onlyIfAbsent =false)
                            putTreeVal方法有三个操作:
                                1.如果刚开始转换树,即开始树不存在,则直接设置新的节点为根节点并返回
                                2.如果树已经存在,则查找key相等的节点,为后续做值覆盖操作做准备
                                3.如果找不到key相等的节点,插入新节点,重新调整红黑树使其平衡
                        */
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {//分支4-3
                /*
                 * 链表节点大于等于8个,转换成红黑树
                 * */
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

putVal方法代码比较多,首先,根据源代码,我们知道:
待添加的节点key已经过散列运算后得到值标记为变量 hash=spread(key.hashCode()),
数组的长度为 n = table.length,
待添加的节点key所在的hash槽数据为node = table[hash & (n-1)];
下面根据代码标注的几大分支一一讲解:

分支1:table数组为空,说明承载元素的table还未初始化,先进性初始化;

分支2:table数组不为空,找到待添加的节点key值所在的hash槽位置 node = table[hash & (n-1)]的数据,
如果node为空,使用CAS设置table[index]为新增的节点,设置成功,退出,否则重新循环

分支3:table数组不为空,node节点的hash的值正好是MOVED,即-1;从MOVED常量的注释可知,node此时是一个ForwardingNode节点,该分支后续再详细介绍。

分支4:该分支处理的是node为一个正常存放数据节点时的情况:
4-1:node.hash >=0 说明node是一个真实的数据节点,采用拉链法添加节点(尾插法)
4-2:node 是一个红黑树结构TreeBin,则往红黑树中添加节点
4-3:添加节点完毕,判断table[hash & (n-1)]槽上节点数是否满足链表 -> 红黑树的转换(链表节点>=8)

最后,节点数增加1.

添加节点整体流程图如下:
在这里插入图片描述

链表添加节点:
在这里插入图片描述

现在,我们再回过头来看看前边的几个疑问:
1.添加节点时如何保证添加成功?
putVal方法内添加节点时用的for循环,保证节点添加成功后才退出
2.如何保证线程安全 ?
现在我们只能回答其中的一部分:当hash发生冲突时,添加节点线程在操作前先使用了synchronized对hash槽加锁,保证了多线程添加操作下串行的执行;
同时我们应该注意到Node结构中next节点使用了volatile修饰,因此,当多个线程添加节点时,等待获取synchronized同步锁的线程在获取到锁后遍历链表进行节点添加时
能直接看到链表发生了变化(tail发生了变化)
3.当一个节点key刚添加成功,此时触发了扩容,而正好又有一个线程调用get(key)想要获取key的值,此时能获取到嘛?
如果能获取到,map是怎么保证正在扩容时也能查询到数据的?
刚添加完节点,另外的线程是可以读取到的。
在这里插入图片描述

在计算待添加节点key所在的hash槽位置时,我们看到并不是简单的 key.hashCode & (n-1) ,而是调用了一个
spread(key.hashCode());为何要根据key.hashCode 再计算一次散列值?带着这个问题,我们看看spread()函数代码

spread()散列计算

/**
 *
 * Spreads (XORs) higher bits of hash to lower and also forces top
 * bit to 0. Because the table uses power-of-two masking, sets of
 * hashes that vary only in bits above the current mask will
 * always collide. (Among known examples are sets of Float keys
 * holding consecutive whole numbers in small tables.)  So we
 * apply a transform that spreads the impact of higher bits
 * downward. There is a tradeoff between speed, utility, and
 * quality of bit-spreading. Because many common sets of hashes
 * are already reasonably distributed (so don't benefit from
 * spreading), and because we use trees to handle large sets of
 * collisions in bins, we just XOR some shifted bits in the
 * cheapest possible way to reduce systematic lossage, as well as
 * to incorporate impact of the highest bits that would otherwise
 * never be used in index calculations because of table bounds.<br/>
 */
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

原文大段注释大概意思是:采用一般的key.hasCode【对象的hashCode返回的是一个int型数据,即32位的二进制数据】进行散列,当hash数组容量n比较小时,因为数组长度的限制,key.hashCode & (n-1) 只能使用到key.hashCode的最后几位,这个时候产生hash冲突的概率将会加大,因此,为了使得容量较小时减少发生冲突的概率,将 key.hashCode的高16位左移至低16位,并进行异或运算,最后得出的新的 hashCode,低16位同时拥有了高位和低位的特性,用这样的方式来减少冲突的发生;而当数组容量n非常大,n 大于 1<<16时,key.hashCode & (n-1),当 key.hashCode 高16位一致情况下,散列的分布同样由低16位特性决定。
总的意思就是,通过将 key.hashCode的高16和低16位进行异或,增加了hash的随机性,达到减少冲突概率的目的

初始化数组 initTable()

/**
 * 初始化Map的table数组
 */
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    /*
    * 初始化table数组时,sizeCtl有两种取值可能:
    *  sizeCtl = -1,数组正在初始化中
    *  sizeCtl = 0
    *  sizeCtl >0 :说明是 ConcurrentHashMap构造时指定了容量,但是table还未初始化
    * */
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // tab == null,sizeCtl=-1说明tab已经有一个线程正在初始化tab,当前线程执行yield,释放cpu时间片,从运行态变成就绪态
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            /*
            * 数组为null,当前线程CAS操作修改sizeCtl=-1,成功说明当前线程拥有初始化table的执行权力,开始对tab进行初始化
            * */
            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);//位运算:数组长度n无符号右移2位为n/4;则sc=3n/4 = 0.75n;所以下次触发扩容的点是0.75*16=12
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

初始化数组时有这么一段:int n = (sc > 0) ? sc : DEFAULT_CAPACITY; sc指的是当我们使用指定容量构造函数时生成的初始化容量值,我们再来看看该构造函数:

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

sizeCtl是根据给定的初始化容量来计算tableSizeFor得出

tableSizeFor()计算table的适合容量

/**
 * 
 * 返回最接近c的2的幂次方的数,这里使用了位移算法
 * 
 * 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;
}

函数内一堆位移操作,我们来一一分析:
举例:假如c=20 ,n=c-1=19;二进制为 0001 0011;

n |= n >>> 1 : n右移1位 0000 1001 与n做或运算为 n = 0001 1011; 最高位1的后面一位变成了1,此时n自高位1起顺延低位多了一个1

n |= n >>> 2 : n右移2位 0000 0110 与n做或运算为 n = 0001 1111; 右移2位后等于原始产生的两个高位1移动了2位,此时与n或运算后得出自第一个高位1起一共4位变成了1

n |= n >>> 4 : n右移4位 0000 0001 与n做或运算为 n = 0001 1111;



由此可知,该算法的目的是把原始n值从高位第一个1 起,将后面的所有位全部置为 1;即n=16时,一顿操作后 从 0001 0000 >>> 0001 1111

分支4-3中,添加节点完成后,判断链表节点个数是否已经满足转换成红黑树的要求(>=8),满足则进入转换环节

链表-红黑树相互转换

链表进化成红黑树(节点数>=8)

当由于hash冲突导致一个hash槽内的链表节点数>=8个时,链表结构将会进化成红黑树,但是链表的树化与否还由table数组的长度决定,我们先来看看代码:

/**
 * 对应table[index]槽的链表结构升级成红黑树结构
 * @param tab
 * @param index
 */
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        //只有当hash数组table容量达到64,且链表的长度为8时才会树化,否则只是进行扩容
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY) //分支1
            /*
            * 假设n此时为16,进入tryPreSize时入参即变成32,
            * 而tryPresize(size) 方法会根据size来再一次升级,因此,
            * 假如n=16,且此时有一个hash槽上的链表节点达到了8个,此时会一次性扩容到 64
            * */
            tryPresize(n << 1);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {//分支2
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    /*
                        treeNode构建时我们能看出是一个双向链表,而且,虽然构造了一棵树,但是它还保留了原始链表的从属关系(A-B节点前驱后继关系是保留着的,保留原始链表关系对后面的操作非常有用)
                    */
                    for (Node<K,V> e = b; e != null; e = e.next) {
                    	/*
							这里构建TreeNode节点,调用的是
							TreeNode(int hash, K key, V val, Node<K,V> next, TreeNode<K,V> parent)构造函数,此时next为null;
						*/
                        TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                        null, null);
                        if ((p.prev = tl) == null)
                            hd = p;//p的前驱节点tl如果为空,tl为空,说明此时是第一个节点,直接设置为头节点hd
                        else
                            tl.next = p;//否则tl后继指针为p
                        tl = p;
                    }
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

链表树化整体流程:
在这里插入图片描述

链表树化主要分成两大分支:
分支1:首先看当前hash数组tab的容量是否已经达到64,如果小于64,则只进行扩容操作tryPresize(),后续扩容环节将详细介绍。
分支2:hash数组容量已经达到64,index位置上的结构是一个节点数>=8个的链表结构,开始构造树节点,而且看了代码和注释应该能知道,红黑树在构造的时候还保留了原始链表结构的从属关系,这个细节对于后面的一系列操作非常有用,先mark一下,链表转成红黑树后整体结构如下图(红黑树动态演示可以点击此网址查看):
在这里插入图片描述
在这里插入图片描述

红黑树退化成链表(节点数<=6)

将红黑树中树结构退化成链表结构,从树结构特性可知,TreeNode还保留了原始Node的链表连接状态,因此,直接按照 node.next后继节点逐个遍历后构建节点即可,根据链表树化时的结构,只要从TreeBin内部保存的首节点 first节点开始遍历,就能得到所有节点,构成链表。

/**
 * 树结构转换成链表结构
 *
 * Returns a list on non-TreeNodes replacing those in given list.
 */
static <K,V> Node<K,V> untreeify(Node<K,V> b) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = b; q != null; q = q.next) {
        Node<K,V> p = new Node<K,V>(q.hash, q.key, q.val, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}

扩容

尝试扩容

tryPresize() 方法在 putAll() 和 treeifyBin()中调用,我们一一来分析每个方法中调用tryPresize()时的情况。
1.putAll()方法内调用:能调用putAll()方法前肯定先进行了一番CHM的构造,那么此时又分为以下几种:
1.1 调用了无参构造函数,此时 sizeCtl = 0; hash数组tab[]为null
1.2 调用了指定容量的构造函数,此时 sizeCtl = tableSizeFor()计算后得到的值 >0
因此,在调用尝试扩容方法时,还需要考虑table数组的初始化问题

/**
 * @param initialCapacity 初始容量
 */
public ConcurrentHashMap(int initialCapacity) {
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
            MAXIMUM_CAPACITY :
            tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

2.treeifyBin()方法内调用:只有当hash数组table的长度未达到64时,调用tryPresize()方法,此时的 sizeCtl=loadFactor * table[].length = 0.75*n;

因此,tryPresize()方法内可能要做的事情应该有:
1.根据入参size计算出即将扩容到的量级
2.如果数组没有初始化,则先进行table初始化
3.开始扩容
接下来,我们来看看tryPresize()的代码。

/**
 * 尝试对tab扩容
 *
 * @param size number of elements (doesn't need to be perfectly accurate)
 */
private final void tryPresize(int size) {
    //n长度扩容成2n
    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;
        /*
            tab ==null 说明还未初始化,则尝试对tab进行初始化
        */
        if (tab == null || (n = tab.length) == 0) {
            n = (sc > c) ? sc : c;//n 为最新的容量
            //当前线程尝试对 sizeCtl  0 -> -1 ,cas成功当前线程获得对tab初始化权力
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if (table == tab) {
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        sc = n - (n >>> 2);//sizeCtl 设置为 0.75倍的容量
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        /*
        * 最近计算的扩容容量c <sizeCtl,说明本次尝试扩容的过程中已经有其他完成对数组容量扩充,且扩充到了更高的容量,退出
        * 或者是一开始指定了容量,后调用putAll()方法只一次性设置了很少的数据,导致c<初始指定的容量,此时也说明不需要扩容
        * */
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        else if (tab == table) {//数组不为空,说明可能有线程正在初始化数组,或者数组已经有了数据,开始进行扩容,并转移数据
            int rs = resizeStamp(n);//获取基于本次扩容前容量n的一个时间戳(版本号)
            if (sc < 0) {
                /*sc<0可能是 -1,此时说明有线程在执行初始化扩容中
                  sc为很大的负数,说明是某一次容量扩容时 rs << 16位后的值(低16位最后变成高16位,且rs最小是 2^15 +n,高16首位为1,为负数)
                  sc >>> RESIZE_STAMP_SHIFT 右移16位后将高16位变成了低16位,正好是rs的值,
                  (sc >>> RESIZE_STAMP_SHIFT) != rs :正常情况下 sc右移后应该会等于扩容为n时的rs值,不等说明当前扩容已经不是原来的tab.length,长度已经发生变化(正在扩容的量已经改变)
                  sc == rs + 1 ?这个不太懂,这里是不是应该改为 sc == (rs << RESIZE_STAMP_SHIFT) + 1
                  sc == rs + MAX_RESIZERS 说明已经达到最大的参与扩容线程数 ? 这里是否该改为 sc == (rs << RESIZE_STAMP_SHIFT) + MAX_RESIZERS

                  至于,这里先解释一下发生的情况:
                    (nt = nextTable) == null,transferIndex <= 0 是因为扩容转移节点transfer()方法内
                    需要先初始化nextTable 和对 transferIndex 赋值,完成后(nextTable !=null ,transferIndex > 0)才开始进行数据转移,所以这里为空则
                    表示刚刚开始进入transfer的初始化新数组阶段.
                  因此  sc<0 && (nextTable == null || transferIndex <= 0) 可能的情况即为:有其他线程已经开始了n->2n或者更大容量的扩容操作,我们可以退出了;
                */
                Node<K,V>[] nt;
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                    break;
                /*
                    当sc<0时,说明此时已经在扩容中,而且是基于原始容量n的扩容,U.compareAndSwapInt(this, SIZECTL, sc, sc + 1),设置sc=sc+1,标识当前帮助扩容的线程数
                    sc+1 这个负数会变得也来越大,每次扩容完毕后sc=sc-1,直到 sc=(rs << RESIZE_STAMP_SHIFT)+2 为止
                */
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            /*
                当sc>0时,说明此时sc=sizeCtl存放的正好是触发扩容时的容量值,此种情况说明,扩容还未开始,当前线程可以开始启动扩容操作。
                设置为一个很大的负数值(原始容量n的一个版本号+线程数) sc = (rs << RESIZE_STAMP_SHIFT) + 2;标识当前线程正在做扩容操作
                rs << RESIZE_STAMP_SHIFT 左移16位后 sc高16位为rs的值,第16位保存帮助扩容的线程数。

                rs << RESIZE_STAMP_SHIFT) + 2 : 这里先留个疑问,为什么是+2? 答案就在transfer()方法里
            */
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                    (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}

我们来重点分析下数组不为空时触发扩容几个重要步骤:
1.获取基于本次扩容前容量n的一个时间戳(版本号): rs = resizeStamp(n),
2.sc=sizeCtl,而sizeCtl根据介绍,当 sizeCtl > 0 时,存放的就是触发扩容时的阈值 0.75*n;
当sizeCtl < 0 时,说明存放的时扩容时的一个值,是什么呢?我们看到 sizeCtl > 0 时,执行了

else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
    transfer(tab, null);

那么 sizeCtl < 0 时,就是已经开始了从 n->2n 的扩容,且sizeCtl字段内存放了两部分的值:rs << RESIZE_STAMP_SHIFT 和 2;由此我们可知,sizeCtl高16位存放的是版本号rs;那么这个 +2 是什么呢?
不知道大家还记不记得 sizeCtl字段的介绍:当发生扩容时,sizeCtl为负数,且存放了参与扩容的线程数。那么我们就可以断定,sizeCtl低16位就是存放的参与扩容的线程数,最大可以达到 2^16 - 1 个线程,这个2就是线程数,而且开始扩容时就设置为有2个线程。
因此,在sizeCtl < 0 时,先进性了一系列的合理性判断,最后执行

if (sc < 0) {
    ......
    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
        transfer(tab, nt);
}

U.compareAndSwapInt(this, SIZECTL, sc, sc + 1) 即如果说扩容操作已经开始了,那么当前线程就将参与扩容线程+1,然后i将纳入transfer()

大家有没注意到 treefyBin()方法再调用tryPresize()时的入参,假设容量为默认的初始值,n=16 < 64,我们开始尝试扩容,此时入参变成了size=32,
而tryPresize()方法内会再一次计算最新的容量值,c= tableSizeFor()直接返回了64。
可是transfer()方法并没有使用按照64来进行,而是又一次重新计算,其实使用的是32。
等待transfer()执行完成,此时 (sc = sizeCtl = 2n * 0.75 = 24) >=0 ;而且,sc < (c = 64) ,将会再一次调用transfer(),这一次table从32变成64长度,sizeCtl=640.75=48;
transfer()执行完成,此时sc=sizeCtl=48;还是由 sc<(c = 64),再次执行transfer(),这一次table从64变成128长度,转移完成后sizeCtl=128
0.75=96;
最后sc>c,退出while循环。这里经过多次转移节点才退出,效率上是否会有问题呢?不过这种情况属于极端情况(即添加了8个节点都恰好落在同一个槽内)

带着tryPresize()内的几个疑问:
1. (nt = nextTable) == null,transferIndex <= 0 是什么情况下发生
2.开始扩容时,为什么一开始设置的值 U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2) ,为什么是+2,不是加其他的数字?

节点转移transfer(扩容)

扩容,整体分成两步:
1.hash数组容量扩大 n -> 2n
2.遍历原始hash数组上所有节点,并进行节点rehash,确定要放入新数组的位置,即由原来的 spread(key.hashCode) & (n - 1) 变成了 spread(key.hashCode) & (2n - 1),
放入新位置上,如果发生冲突,采用拉链法或树化法。

扩容前

在这里插入图片描述
扩容后
在这里插入图片描述

看了上面扩容的流程,是不是感觉很简单?说实话,就是这么的简单。
但是,hash扩容本身就是一个很耗时的操作,如果按照上面的方式,假设扩容前有n个节点数据,扩容时所有节点从头遍历一遍,每个节点都rehash, 复杂度O(n),效率上很差
1.那有没有效率更高的方式呢?

2.使用多个线程并行处理扩容可以吗?如果可以,数据安全怎么保证,多线程环境下的竞争怎么处理?

3.通常情况下所有节点都需要rehash才能确定新的存放位置,那么有没有其他方式,不需要rehash所有节点?

带着这几个问题,我们来看看Doug Lea大神的神奇扩容做法(多线程扩容,多线程分段转移节点;hash槽上节点重用,减少不必要的创建;扩容过程中同时支持节点查询和更新;扩容转移节点时从后往前n-1…0):
The table is resized when occupancy exceeds a percentage threshold (nominally, 0.75, but see below).

1.触发扩容的条件:数组占用率超过百分比阈值0.75时触发扩容

Any thread noticing an overfull bin may assist in resizing after the initiating thread allocates and sets up the replacement array.

2.当数组在扩容时,任何线程操作table后(添加节点)发现数组占用率(0.75),都会触发扩容,因此transfer的过程是多线程进行的

However, rather than stalling, these other threads may proceed with insertions etc.

3.扩容时,添加节点可以正常进行,而不是停止等待扩容完成(如线程1在table[0]位置上添加了一个节点后触发了扩容,此时线程1正在transfer中,这时如果线程2执行了put操作添加节点,是可以正常添加的)

The use of TreeBins shields us from the worst case effects of overfilling while resizes are in progress.
Resizing proceeds by transferring bins, one by one, from the table to the next table.

4.扩容的过程是将原数组上的槽一个接一个的转移到下一个新的数组(nextTable)中,即有序的。

However, threads claim small blocks of indices to transfer (via field transferIndex) before doing so, reducing contention.

5.扩容时,由于支持多线程同时进行,那么为了减少线程间对hash槽不必要的竞争,每一个线程每次转移的hash槽,必须相互隔离,也就是每个线程负责单独的一个或者几个(即每个线程一次负责一个transfer块)

A generation stamp in field sizeCtl ensures that resizings do not overlap.

6.扩容总是从n -> 2n;因此,每一次触发扩容时,会根据原始长度 n生成一个令牌(版本号)标识此次扩容,防止发生重叠,这个版本号放置在 sizeCtl中【具体怎么放?】
(我们设想一下,如果不对每一次扩容做标记,任何线程都触发的情况下,所有线程都从头开始;或者在n->2n过程中,有另一个线程触发的是 4n或者更多的扩容操作,此时会出现什么问题?)

Because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset.

7.扩容时每次都是以2幂次扩张,所以rehash时元素有这些特性:要么还是在原来的位置,要么rehash之后被放在了原来位置增加2幂次的位置
这里解释下,假如扩容前为16,扩容到32,rehash算法从 hash & (n-1) 变到了 hash & (2n-1),也就是 hash & 0000 1111 变成了 hash & 0001 1111,如果hash=1;
那么rehash后从 1 变成了 17,其实就是 1+n的地方,神奇的操作。

We eliminate unnecessary node creation by catching cases where old nodes can be reused because their next fields won’t change.
On average, only about one-sixth of them need cloning when a table doubles.

8.根据第7点解释,当节点转移时,某些场景下,节点可以重用,不需要再进行rehash,直接转移,减少了节点的创建;
而且平均来说,当table扩容到原来的2倍时,只有 1/6 的节点需要克隆

The nodes they replace will be garbage collectable as soon as they are no longer referenced by any reader thread that may be in the midst of concurrently traversing table.

9.当节点被转移后,原始数组上对应的hash槽,如果没有任何引用,将随时被当作垃圾进行回收

Upon transfer, the old table bin contains only a special forwarding node (with hash field “MOVED”) that contains the next table as its key.
On encountering a forwarding node, access and update operations restart, using the new table.

10.在转移节点时,旧的数组table上对应的hash槽将只包含一个特殊的标记节点ForwardingNode,这个节点的hash属性值为 MOVED,且该 ForwardingNode 节点内包含了扩容时的新数组nextTable,此时所有的查询的更新操作都在新的数组nextTable上进行。
(大家想想为什么特殊节点上需要有nextTable的引用?其实是为了查询操作设置的,设想一下原始数组table[1]被转移完成后,标记成了table[1]=ForwardingNode,如果还未扩容结束,此时另一个线程查找key元素正好是在table[1]上,
如果ForwardingNode内部不引用扩容时新nextTable,此时再也没地方进行查找了,直接返回null?不能够的,大哥)

Each bin transfer requires its bin lock, which can stall waiting for locks while resizing.
However, because other threads can join in and help resize rather than contend for locks, average aggregate waits become shorter as resizing progresses.

11.hash槽上的节点转移时都需要锁,因此扩容时可能会出现锁等待的情况。不过我们采用了多线程分段转移节点的方式,线程间相互隔离,避免了锁争抢,因此扩容操作的效率得到了提高

The transfer operation must also ensure that all accessible bins in both the old and new table are usable by any traversal.
This is arranged in part by proceeding from the last bin (table.length - 1) up towards the first.
Upon seeing a forwarding node, traversals (see class Traverser) arrange to move to the new table without revisiting nodes.

12.transfer的过程中必须要保证所有的节点都可以访问,或者能遍历到(这里的遍历指的是map的遍历,其实就是 Iterator,如 keySet(),valueSet,Entry等,他们都是索引从小到大0…n-1的遍历table),因此为了减少不必要的冲突,扩容转移节点时采用的是从大到小(n-1…0)
进行,当遍历时碰到ForwardingNode节点,则转到扩容时新数组nextTable上进行。

To ensure that no intervening nodes are skipped even when moved out of order, a stack (see class TableStack) is created on first encounter of a forwarding node during a traversal, to maintain its place if later processing the current table.
The need for these save/restore mechanics is relatively rare, but when one forwarding node is encountered, typically many more will be.
So Traversers use a simple caching scheme to avoid creating so many new TableStack nodes. (Thanks to Peter Levart for suggesting use of a stack here.)

根据两大段话的解读我们知道扩容时,每个进入transfer()的线程一次领取一段,大小最小为16,直到没有可分配的段后,等待所有节点都转移完成,则标识扩容完成。分配方式如下图:
在这里插入图片描述

下面我们来看看大神的扩容

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    /*
    * 根据CPU核数分配,一个线程最小帮助扩容的hash槽数MIN_TRANSFER_STRIDE = 16
    *  n/(8*NCPU),
    * */
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        /*
            nextTabe == null,说明还未开始扩容,则初始化nextTab,每次调整为原来的2倍
        */
        try {
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    /*
        advance : 变量标识着扩容时每一个tranfer块是否可以迁移任务块中hash槽的待迁移节点;
        每一个transfer块最小包含16个hash槽,一个tranfer块仅允许一个线程来做节点的迁移
    */
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    /*
        bound : 变量存放的是一个transfer块内最小的hash槽下标位置,因为hash槽迁移的过程是从右往左,
        分配transfer的过程也是从大到小,反向的迭代(相当于一个队列,每次分配transfer块后,队列元素个数减1,每次都从队尾开始出元素)

        i:是一个transfer块中当前待转移到新数组中下标  table[i]
    */
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        /*
            advance默认可以进入,算是一个预处理的过程,while循环内逻辑可以总结为:
                1. 判断扩容是否完全结束
                2. 对单个transfer块内迁移的hash槽下标 i 进行移动,达到控制迁移的目的
                3. 分配transfer块给来帮助作扩容的线程,同时确定transfer块内可操作hash槽的上下界 [bound,i]
        */
        while (advance) {
            int nextIndex, nextBound;
            /*
                finishing =true : 判断是否已经扩容完毕
                --i >= bound : 单个transfer块的迁移任务,hash槽的迭代是从大到小来进行的,所以这里两步操作:左移hash槽的下标并判断是否达到tranfer块迁移的下临界值
                如果移动后还未达到下临界值,说明可以继续进行当前transfer块内下一个hash槽的节点迁移
            */
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                /*
                    transferIndex<=0 说明扩容时所有的hash槽都已经被执行扩容的线程领取完毕,已经没有可以分配的transfer了(队列元素已经全部出队完毕,size=0了)
                    直接置 i=-1并退出循环, -1是为了走while外的if逻辑做准备
                */
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                    (this, TRANSFERINDEX, nextIndex,
                            nextBound = (nextIndex > stride ?
                                    nextIndex - stride : 0))) {
                /*
                    cas操作,分配transfer块时每次对transferIndex 递减stride个(最小是16),操作成功的线程说明得到该tranfer块节点迁移权
                    接着确定分配的transfer块上下界 i可操作最大下标 ,bound为可操作最小下标值
                */
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        /*
            i<0 : 前面分析到 i<0 可能会再两种情况下出现 1.transferIndex <=0 时 扩容的transfer块已经全部分配完毕;
                2.最后一个transfer任务 正常迁移完毕后 --i 从 0变成 -1,说明所有的迁移都已经结束了

            i >= n : 正常情况下 i最大就是  transferIndex -1 = n - 1,不会大于n,如果出现大于n,说明我们在进行扩容时可能由于某些原因暂时停止后重新进行扩容,等待再次分配得到一个tansfer块时,i>n
            ,但是此时tab.length 已经不是原来的长度,可能已经变成了 4n或者更多,此时线程获得了迁移节点的权力,但是已经不是当初的那个扩容状态(如 n >> 2n时的状态)

            i +n >= nextn ? 不知道啥条件下会发生
        */
        if (i < 0 || i >= n || i + n >= nextn) {//判断扩容完成与否分支
            int sc;
            if (finishing) {
                /*
                    finishing =true 只有在全部扩容完成后才会设置为true,也就是下面的if逻辑;扩容完成后,把tab变成了 nextTab,容量变成了原来的2倍2n;
                    此时 sizeCtl = 2n * 0.75
                */
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            /*
                CAS操作尝试对 sc-1, 即对参与扩容的线程-1,表明当前线程迁移节点完毕,尝试退出本次扩容操作
                CAS操作成功,则检查 sc是否等于扩容时的状态(sc= (rc = resizeStamp(n)) << RESIZE_STAMP_SHIFT),
                这里有两种情况:1.如果相等则表明所有参与本次扩容的线程都已经执行完毕,且当前线程是最后一个执行trasfer块迁移动作的终结者
                                设置finishing = advance = true; 扩容完毕标志位为完成

                                2.如果不等,则说明当前还有线程还未执行完成迁移tansfer任务,只做当前线程的退出操作 return
            */
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                /*
                    i = n ,这里做了一次兜底操作,i设置为n,循环全部检查一遍tab所有的节点是否已经全部被迁移完毕(tab所有的节点都变成 ForwardNode(hash=MOVED=-1)),
                    如果有的节点没有变成ForwardNode(可能其他transfer块在迁移的过程中发生了异常导致没有迁移完毕),当前线程再去执行迁移动作,防止有漏网之鱼
                */
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)//hash捅本身就是null,则直接设置一个转移节点
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)//hash捅本身已经设置为转移(即被转移)
            advance = true; // already processed
        else {
            /*
             * rehash转移原来的table[i]完毕后,newTable上已经放置新的节点链完成,才会将 table[i] 置为 ForwardingNode,
             * 因此,如果说在get()某一个刚刚put完成后触发扩容的节点,在节点未转移完成前, 还是会在原来的table[i]中get,
             * 如果发现table[i] 上节点为 ForwardingNode,则去到  nextTable 查找
             * */
            synchronized (f) {
                //对要转移的节点加锁,又判断一次,典型的DCL(double check lock)
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {//fn>0,说明当前hash槽内是链表结构

                        /*
                            ln, hn :这里定义了 lowerNode 和 higherNode两节点,分别用来存储后续rehash之后那些应该放在低位(nextTab 低位index),那些应该放在高位(nextTab 高位index)
                            runBit = fh & n :fh=Node.hash,当前链表内的节点能放置在同一个hash槽tab[index] 内说明 Node.hash & (n-1) 之后的值是一致的,即Node.hash最后几位是一致的。
                            如:n=16, n-1 二进制则为 00001111,则说明tab[index] 内 Node.hash后4位都是一样的,所以,runBit = fh & n 只可能有两种结果 00010000 / 00000000;
                            当前nextTab[] 长度为 2n,2的幂次数,所以tab从n变到2n时,rehash时其实只需要再关注二进制的上一位即可
                            (如n=16变成32,则 node.hash & (n -1) 仅仅只是从 node.hash & 00001111 变成了 node.hash & 00011111,所以,关注第5位是0还是1就可以知道rehash之后的 hash槽在高位还是低位)

                            由上面分析可知,这个for循环的目的是想找到链表中的一个临界节点,该临界节点及之后的其余节点的  runBit = fh & n 都是相同的,可以直接转移,
                            不需要再迭代了(节点重用,效率更高);而临界节点前的点因为 runBit 不一致,需要循环链表进行重新 高低槽分类
                            例子:hash槽为 n=16,当前链表有4个节点 node(hash=00110111) -> node(hash=00100111) -> node(hash=10110111) -> node(hash=11110111)

                            开始 runBit = 00110111 & 00010000 = 00010000;
                            最后循环退出之后应该是链表第三个节点(runBit = 10110111 & 00010000 = 00010000 ; lastRun = node(hash=10110111)),
                            第3个节点及以后的节点二进制第5位都是一致的,0/1,所以rehash之后的位置肯定也还是在同一个地方,出于效率考量,这部分节点直接转移,不用迭代,

                            lastRun:经过下面的循环之后,lastRun可能还是一个链表
                        */
                        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;
                            }
                        }
                        //runBit == 0 说明扩容后还是放在原来的下标槽内
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        //以lastRun为临界点,从头开始迭代链表,根据 node.hash & n 是否为0来区分节点rehash之后高低槽分类
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        //ln,hn高低槽分类完毕后,设置到扩容后的数组中,原有数组hash槽设置为forwardNode节点,标识已被转移
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        /*
                            如果已经为红黑树,遍历所有节点,并开始分类,
                            lo:低位hash槽,即原来tab[i](node.hash & (n -1) == 0) 现在还是 在tab[i]
                            hi:高位hash槽,即(node.hash & (n -1) != 0)不为0,说明n变为2n之后 hash散列处于高位

                        */
                        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;
                        /*
                            first即为链表的第一个节点
                        */
                        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);
                            /*
                                h & n==0 说明从n -> 2n 后 hash & (n-1) == hash &(2n - 1),
                                归为低位
                            */
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {//高位hash槽
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        /*
                         重新归类后低位hash节点数 <=6 ? 转换成链表
                         >6 构造一棵新红黑树,如果rehash后归高位hash槽的节点数为0
                         说明rehash之后不变,则直接将原始的红黑树分给低位槽;
                         高位hash槽同理
                        */
                        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;
                        //ln,hn高低槽分类完毕后,设置到扩容后的数组中,原有数组hash槽设置为forwardNode节点,标识已被转移
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

虽然上边代码我已经写了很多的注释,不过可能看的比较晕,下面,我们将整个transfer分割成几部分,分段进行讲解。

1.确定一个批次转移节点的个数stride

private static final int MIN_TRANSFER_STRIDE = 16;//扩容线程每次负责转移的hash槽的最小个数

if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; 

我们把一个线程一次负责转移hash槽的动作称为一个批次,n为扩容前的数组容量,首先根据可用CPU核心数,确定一个批次负责的hash槽的数量,
CPU核心数有多个时,取 n/(8* NCPU),最后与 最小单批次负责转移的hash槽数比较,取较大者。

2.初始化存放新的节点的hash数组nextTab,nextTab长度为原始数组的2倍

if (nextTab == null) {// initiating
    /*
        nextTabe == null,说明还未开始扩容,则初始化nextTab,每次调整为原来的2倍
    */
    try {
        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
        nextTab = nt;
    } catch (Throwable ex) {      // try to cope with OOME
        sizeCtl = Integer.MAX_VALUE;
        return;
    }
    nextTable = nextTab;//nextTable只在扩容时不为空
    transferIndex = n;
}

nextTab为空,先进行初始化,容量即为原始容量2倍 2n。初始化完成后sizeCtl还是原来的值,这里需要注意,只有扩容完成后sizeCtl才会变成最新的值。
赋值 nextTable = nextTab;nextTable是一个内存可见保证的 volatile 变量,当发生扩容时 nextTable 才不会为空,用于扩容过程中存放rehash后节点数据。
transferIndex存放的是扩容时下一次可以转移的hash槽的下标+1;即如果n=32,那么首次开始时transferIndex=32

3.构建ForwardingNode标记节点

ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

ForwardingNode标记节点内持有了扩容时节点存放的新数组nextTab,保证查询和更新操作可以有地可寻

4.分配线程执行节点转移的transfer段

boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
    Node<K,V> f; int fh;
    /*
    advance默认可以进入,算是一个预处理的过程,while循环内逻辑可以总结为:
              1. 判断扩容是否完全结束
              2. 对单个transfer块内迁移的hash槽下标 i 进行移动,达到控制迁移的目的
              3. 分配transfer块给来帮助作扩容的线程,同时确定transfer块内可操作hash槽的上下界 [bound,i]
      */
    while (advance) {
        int nextIndex, nextBound;
        /*
            finishing =true : 判断是否已经扩容完毕
            --i >= bound : 单个transfer块的迁移任务,hash槽的迭代是从大到小来进行的,所以这里两步操作:左移hash槽的下标并判断是否达到tranfer块迁移的下临界值
            如果移动后还未达到下临界值,说明可以继续进行当前transfer块内下一个hash槽的节点迁移
        */
        if (--i >= bound || finishing)
            advance = false;
        else if ((nextIndex = transferIndex) <= 0) {
            /*
                transferIndex<=0 说明扩容时所有的hash槽都已经被执行扩容的线程领取完毕,已经没有可以分配的transfer了(队列元素已经全部出队完毕,size=0了)
                直接置 i=-1并退出循环, -1是为了走while外的if逻辑做准备
            */
            i = -1;
            advance = false;
        }
        else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
            /*
                cas操作,分配transfer块时每次对transferIndex 递减stride个(最小是16),操作成功的线程说明得到该tranfer块节点迁移权
                接着确定分配的transfer块上下界 i可操作最大下标 ,bound为可操作最小下标值
            */
            bound = nextBound;
            i = nextIndex - 1;
            advance = false;
        }
    }
    
    .....转移节点代码
}

从while()逻辑中我们总结几个变量的使用:
advance:是否可以向前推进,即 advance=true,说明可以向前,如转移了table[n-1]完成后,是否可以向前分配到table[n-2]
nextIndex:下一次分配transfer段内最大hash数组索引index;即如果数组长度为32,扩容到64,那么首次分配时第一个transfer最大nextIndex=32,下一次就是 nextIndex=16
nextBound:下一次分配给transfer段内最小hash数组索引index;即如果数组长度为32,扩容到64,那么首次分配时第一个transfer最大nextBound=16,下一次就是 nextBound=0
这里我们就可以知道,一个transfer分配的方式 (32,16],(16,0)…
transferIndex:扩容时,可以开始分配的index,它是一个 volatile变量,如果数组长度为32,扩容到64,那么首次可分配的transferIndex=64,分配完一个transfer给扩容线程后,transferIndex=16
请看下图:
在这里插入图片描述

先看看正常流程
5.节点转移- 链表节点

/*
 * rehash转移原来的table[i]完毕后,newTable上已经放置新的节点链完成,才会将 table[i] 置为 ForwardingNode,
 * 因此,如果说在get()某一个刚刚put完成后触发扩容的节点,在节点未转移完成前, 还是会在原来的table[i]中get,
 * 如果发现table[i] 上节点为 ForwardingNode,则去到  nextTable 查找(转到了新数组,肯定是要rehash后到指定的位置去查找节点的)
 * */
synchronized (f) {
    //对要转移的节点加锁,又判断一次,典型的DCL(double check lock)
    if (tabAt(tab, i) == f) {
        Node<K,V> ln, hn;
        if (fh >= 0) {//fn>0,说明当前hash槽内是链表结构

            /*
                ln, hn :这里定义了 lowerNode 和 higherNode两节点,分别用来存储后续rehash之后那些应该放在低位(nextTab 低位index),那些应该放在高位(nextTab 高位index)
                runBit = fh & n :fh=Node.hash,当前链表内的节点能放置在同一个hash槽tab[index] 内说明 Node.hash & (n-1) 之后的值是一致的,即Node.hash最后几位是一致的。
                如:n=16, n-1 二进制则为 00001111,则说明tab[index] 内 Node.hash后4位都是一样的,所以,runBit = fh & n 只可能有两种结果 00010000 / 00000000;
                当前nextTab[] 长度为 2n,2的幂次数,所以tab从n变到2n时,rehash时其实只需要再关注二进制的上一位即可
                (如n=16变成32,则 node.hash & (n -1) 仅仅只是从 node.hash & 00001111 变成了 node.hash & 00011111,所以,关注第5位是0还是1就可以知道rehash之后的 hash槽在高位还是低位)

                由上面分析可知,这个for (Node<K,V> p = f.next; p != null; p = p.next)循环的目的是想找到链表中的一个临界节点,该临界节点及之后的其余节点的  runBit = fh & n 都是相同的,可以直接转移,
                不需要再迭代了(节点重用,效率更高);而临界节点前的点因为 runBit 不一致,需要循环链表进行重新 高低槽分类
                例子:hash槽为 n=16,当前链表有4个节点 node(hash=00110111) -> node(hash=00100111) -> node(hash=10110111) -> node(hash=11110111)

                开始 runBit = 00110111 & 00010000 = 00010000;
                最后循环退出之后应该是链表第三个节点(runBit = 10110111 & 00010000 = 00010000 ; lastRun = node(hash=10110111)),
                第3个节点及以后的节点二进制第5位都是一致的,0/1,所以rehash之后的位置肯定也还是在同一个地方,出于效率考量,这部分节点直接转移,不用迭代,

                lastRun:经过下面的循环之后,lastRun可能还是一个链表
            */
            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;
                }
            }
            //runBit == 0 说明扩容后还是放在原来的下标槽内
            if (runBit == 0) {
                ln = lastRun;
                hn = null;
            }
            else {
                hn = lastRun;
                ln = null;
            }
            //以lastRun为临界点,从头开始迭代链表,根据 node.hash & n 是否为0来区分节点rehash之后高低槽分类
            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);//依次构建 ln链,这里变成了头插法,部分节点反转
                else
                    hn = new Node<K,V>(ph, pk, pv, hn);//依次构建 hn链,这里变成了头插法,部分节点反转
            }
            //ln,hn高低槽分类完毕后,设置到扩容后的数组中,原有数组hash槽设置为forwardNode节点,标识已被转移
            setTabAt(nextTab, i, ln);
            setTabAt(nextTab, i + n, hn);
            setTabAt(tab, i, fwd);
            advance = true;//设置为true,会再一次进入while
        }
        ....红黑树节点转移
    }
}

看过代码和相关注释后,我们再来以下面的的链表为例子重新讲解一遍:

假设原始数组长度n=32,即将扩容为64,
当前准备转移的节点为:table[1] = node(1) -> node(33) -> node(65) -> node(97) -> node(129) -> node(161) -> node(225),如图:
在这里插入图片描述
接下来开始遍历一遍链表确定runBit,lastRun 值,

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

过程看图:
在这里插入图片描述在这里插入图片描述
注意看节点6(hash:161)和节点7(hash:225),两个节点 hash & n 之后的值都不等于0,此时lastRun停留在了节点6处,自此,节点6即为一个临界点,就是lastRun变量,分成了两部分:
a) 节点1-5:node(1) -> node(33) -> node(65) -> node(97) -> node(129)节点我们需要重新遍历后确定节点在新数组的位置,

b) 而节点6-7:node(161) -> node(225) 因为hash & n 之后的值都不等于0
紧接着有:

//runBit == 0 说明扩容后还是放在原来的下标槽内
if (runBit == 0) {
    ln = lastRun;
    hn = null;
}
else {
    hn = lastRun;
    ln = null;
}
//以lastRun为临界点,从头开始迭代链表,根据 node.hash & n 是否为0来区分节点rehash之后高低槽分类
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);//依次构建 ln链,这里变成了头插法,部分节点反转
    else
        hn = new Node<K,V>(ph, pk, pv, hn);//依次构建 hn链,这里变成了头插法,部分节点反转
}

runBit如果最后=0,则ln = lastRun ,
否则 hn = lastRun = node(161) -> node(225);
接着开始从链表头开始遍历,直到lastRun之前,还是一样的操作 node.hash & n == 0 ,将遍历到的节点放到 ln链,否则放到 hn链中。为什么能根据 node.hash & n 等于0来确定放在 ln链呢?
扩容前数组容量n = 32;
链表上所有节点都放在table[1],即所有节点 node.hash & n-1 = node.hash & 31 =1;
接着开始遍历 node(1) -> node(33) -> node(65) -> node(97) -> node(129)
在这里插入图片描述
从遍历的效果可以看出,当 runBit = 0时,肯定还是放在原来的索引 1处,而如果 runBit !=0,那么就是放在 1 + n = 33位置处 ;因此,通过 node.hash & n 是否等于0就可以确定是爆出原位置不变还是在 原位置偏移n的索引位置。

同时,大家应该也能看出来,扩容后部分节点的顺序基于扩容前的顺序其实是反转的,只有那些被重用的节点顺序没有变化,如图中所示:
在这里插入图片描述

6.节点转移-红黑树结构

红黑树结构的节点转移其实跟链表一样的方式,只不过需要遍历所有的节点来确定应该放在哪个位置。不知道大家还记不记得,红黑树TreeBin其实不保存节点数据,仅存放的是树的根节点root,而所有节点都是TreeNode结构,又因为TreeNode 其实还保留了Node的特性,即它在变成红黑树之前是一个链表(链表连接关系),变成树之后还保留了链表结构时的指针所属连接关系,所以,遍历红黑树,可以直接采用链表遍历法进行。

else if (f instanceof TreeBin) {
	/*
        如果已经为红黑树,遍历所有节点,并开始分类,
        lo:低位hash槽,即原来tab[i](node.hash & (n -1) == 0) 现在还是 在tab[i]
        hi:高位hash槽,即(node.hash & (n -1) != 0)不为0,说明n变为2n之后 hash散列处于高位

    */
    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;
    /*
        first即为链表的第一个节点
    */
    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);
        /*
            h & n==0 说明从n -> 2n 后 hash & (n-1) == hash &(2n - 1),
            归为低位
        */
        if ((h & n) == 0) {
            if ((p.prev = loTail) == null)
                lo = p;
            else
                loTail.next = p;
            loTail = p;
            ++lc;
        }
        else {//高位hash槽
            if ((p.prev = hiTail) == null)
                hi = p;
            else
                hiTail.next = p;
            hiTail = p;
            ++hc;
        }
    }
    /*
     重新归类后低位hash节点数 <=6 ? 转换成链表
     >6 构造一棵新红黑树,如果rehash后归高位hash槽的节点数为0
     说明rehash之后不变,则直接将原始的红黑树分给低位槽;
     高位hash槽同理
    */
    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;
    //ln,hn高低槽分类完毕后,设置到扩容后的数组中,原有数组hash槽设置为forwardNode节点,标识已被转移
    setTabAt(nextTab, i, ln);
    setTabAt(nextTab, i + n, hn);
    setTabAt(tab, i, fwd);
    advance = true;//循环后会再一次进入while
}

每一个hash槽的节点转移完成之后,都会设置原始数组上对应位置为ForwardingNode,接着设置advance = true;这样等到再一次进入while(advance)时 --i >bound;此时i 索引完成了前进一步,退出while,又继续开始对 i-1处节点转移,直到 i 达到bound为止,一个transfer块执行完毕。

那么,一个transfer块执行完成后,后续又做了什么呢?
我们可以想下,线程进入到transfer()方法后进入的是死循环for (int i = 0, bound = 0; ; )而在一个线程执行完分配给它的16个hash槽后,肯定是不会直接退出,而是会继续检查是否还有可以分配给我的hash槽,因此,退出的条件:
比如所有节点都被分配完,如正在转移节点时发现本次扩容已经过时的(可能由于某些原因,线程暂停了一段时间,最后又重新执行,此时的扩容快照已经不是当时的容量等等)。
接下来我们再仔细看一次下面这个分支:

/*
    i<0 : 前面分析到 i<0 可能会再两种情况下出现 
    	1.transferIndex <=0 时 扩容的transfer块已经全部分配完毕,i被设置为 -1;
        2.最后一个transfer任务 正常迁移完毕后 --i 从 0变成 -1,说明所有的迁移都已经结束了

    i >= n : 正常情况下 i最大就是  transferIndex -1 = n - 1,不会大于n,如果出现大于n,说明我们在进行扩容时可能由于某些原因暂时停止后重新进行扩容,
    但是此时tab.length 已经不是原来的长度,可能已经变成了 4n或者更多,此时线程获得了迁移节点的权力,但是已经不是当初的那个扩容状态(如 n >> 2n时的状态)

    i +n >= nextn ? 不知道啥条件下会发生
*/
if (i < 0 || i >= n || i + n >= nextn) {//判断扩容完成与否分支
	int sc;
	if (finishing) {//默认情况下finishing是false
		/*
			finishing =true 只有在全部扩容完成后才会设置为true,也就是下面的if逻辑;扩容完成后,把tab变成了 nextTab,容量变成了原来的2倍2n;
			此时 sizeCtl = 2n * 0.75
		*/
		nextTable = null;
		table = nextTab;
		sizeCtl = (n << 1) - (n >>> 1);
		return;
	}
	/*
		CAS操作尝试对 sc-1, 即对参与扩容的线程-1,表明当前线程迁移节点完毕,尝试退出本次扩容操作
		CAS操作成功,则检查 sc是否等于扩容时的状态(sc= (rs = resizeStamp(n)) << RESIZE_STAMP_SHIFT),
		这里有两种情况:1.如果相等则表明所有参与本次扩容的线程都已经执行完毕,且当前线程是最后一个执行trasfer块迁移动作的终结者,也就是获得table下标(16,0]槽的转移权的线程
						设置finishing = advance = true; 扩容完毕标志位为完成

						2.如果不等,则说明当前还有线程还未执行完成迁移tansfer任务,只做当前线程的退出操作 return
	*/
	if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {// ==置为扩容完成标识分支==
		if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
			return;
		finishing = advance = true;
		/*
			i = n ,这里做了一次兜底操作,i设置为n,循环全部检查一遍tab所有的节点是否已经全部被迁移完毕(tab所有的节点都变成 ForwardNode(hash=MOVED=-1)),
			如果有的节点没有变成ForwardNode(可能其他transfer块在迁移的过程中发生了异常导致没有迁移完毕),当前线程再去执行迁移动作,防止有漏网之鱼
		*/
		i = n; // recheck before commit
	}
}

上面代码片段有几个关键的地方(置为扩容完成标识分支):
1.我们知道扩容时sc=sizeCtl < 0,而且sizeCtl低16位保存的是扩容的线程数,高16位保存的是基于扩容时的容量n的一个版本号 rs;因此,每个参与扩容转移节点的线程每一次执行完分配给它的16个hash槽之后,会重新请求分配,没有可再分配的hash槽后i被置为-1,或者是获取到最后一个transfer块(16,0]索引的线程执行完毕后,i<0;这个时候执行transfer线程的使命就完成了,需要将参与扩容的线程-1,后退出;

2.为保险起见,最后一个执行完毕的线程(也就是获得table下标(16,0]槽的转移权的线程)还有一次兜底检查所有节点转移情况的任务,防止因为某些情况某个节点没有转移成功 i = n; 重新扫描所有节点,如果所有都转移完,就的数组table上应该全部都是ForwardingNode。

3.注意,在设置i=n时,finishing = true; 此时sc=sizeCtl =(rs = resizeStamp(n) +1,因此,等待兜底扫描完毕后,最后进入 if (finishing) 分支,退出transfer().整个扩容结束。

咱们再来回顾下前边 tryPresize()方法内的几个疑问点:
1.为什么扩容开始时直接设置线程数为2个;

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

2.是否可以参与扩容的判断内有

if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)

现在我们知道原因了:
1.扩容转移节点时有兜底扫描操作,因此开始扩容时线程数+2;
2.当sc=sizeCtl = (rs << RESIZE_STAMP_SHIFT)+1,说明此时的扩容任务正在进入兜底扫描阶段了,也就意味着没有待转移的hash槽分配给你了,自然不需要再进入transfer()方法。

帮助扩容
/**
 * Helps transfer if a resize is in progress.
 */
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);
        /*
            (sc = sizeCtl) < 0 说明map正在执行扩容操作
        */
        while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            /*
                走到这里,说明sc >> RESIZE_STAMP_SHIFT 应该是等于rs(帮助扩容的线程获取当时的n=tab.length未发生变化)
                设置sc=sc+1,标识当前有1个线程参与rehash节点转移操作。
                成功后执行转移操作
            */
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

红黑树

红黑树添加节点

我们知道在put添加节点,如果hash槽位置已经是一颗红黑树TreeBin的时候,是直接在红黑树上添加节点的,现在我们来看看红黑树上添加节点的片段红黑树动态演示可以点击此网址查看

if (tabAt(tab, i) == 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;
		}
	}
}

先回顾下TreeBin数据结构红黑树占位节点TreeBin数据结构,接着看看红黑树添加节点时如何进行的。

/**
 * 新增红黑树的一个节点
 * @param h 节点hash值
 * @param k 节点key
 * @param v 节点key对应的值value
 * @return null if added
 */
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
	Class<?> kc = null;
	boolean searched = false;
	for (TreeNode<K,V> p = root;;) {
		/*
			dir 的取值取决于待新增节点的hash值与树中各节点hash值的大小;该变量是为了区分待新增的节点Node,应该是放在左数还是右树
			dir > 0 到右数寻找; <0 到左数寻找
		*/
		int dir, ph; K pk;
		if (p == null) {
			//root 根节点是空,说明树还未形成,直接把新节点当成根节点
			first = root = new TreeNode<K,V>(h, k, v, null, null);
			break;
		}
		else if ((ph = p.hash) > h)//当前树节点hash大于待新增节点hash值,应该到左子树继续查找
			dir = -1;
		else if (ph < h)//右子树查找
			dir = 1;
		else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
			//根节点的key正好与我们要新增的节点key一致,直接返回命中的节点
			return p;
		else if ((kc == null &&
					/*当k的类型为X,且X直接实现了Comparable接口(比较类型必须为X类本身)时,返回k的运行时类型;否则返回null。*/
				  (kc = comparableClassFor(k)) == null) || 
				  /*kc是实现Comparable接口的类,继续比较两个key大小*/
				 (dir = compareComparables(kc, k, pk)) == 0) {
			/*
				走到这个 else-if块说明:
					1.待添加的节点的key: k实现了Comparable接口,且k.hash 与当前节点相等,但是 k.equals(pk) 为false
					2.待添加的节点的key:k没有实现 Comparable接口
			*/
			
			/*
				第一次循环时,遍历当前节点的左右子树,查找k相等的节点,找不到则返回q = null
			*/
			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;
			}
			
			//如果还是没有找到,这里是当k没有实现Comparable接口时,直接进行判断节点hashcode大小
			dir = tieBreakOrder(k, pk);
		}
		
		/*
			通过上面的一轮判断,确定了dir的值后,这里就能根据dir的值来确定下一步是左子树还是右子树搜寻	
		*/
		TreeNode<K,V> xp = p;//xp保存p下移遍历前的节点
		if ((p = (dir <= 0) ? p.left : p.right) == null) {//当p=null时,说明树的遍历已经倒了终点叶子节点处
			//此时xp为p的父节点,注意这里使用了临时变量f保存了红黑树第一个节点first
			TreeNode<K,V> x, f = first;
			first = x = new TreeNode<K,V>(h, k, v, f, xp);//first指向了当前待新增的节点,红黑树上直接头插法?
			if (f != null)
				f.prev = x;
			if (dir <= 0)//x设为左节点
				xp.left = x;
			else
				xp.right = x;//右节点
			if (!xp.red)//着色,x父节点xp为黑,则x为红
				x.red = true;
			else {
				//往红黑树中添加节点,需要维护额外的锁,因为添加节点后,需要挪动节点使红黑树重新平衡
				lockRoot();
				try {
					//调整使树平衡
					root = balanceInsertion(root, x);
				} finally {
					unlockRoot();
				}
			}
			break;
		}
	}
	assert checkInvariants(root);
	return null;
}

我们能看到,红黑树添加节点无非就是几个步骤:
1.搜寻待添加节点可放置的合适位置
2.如果红黑树失去平衡,调整使其平衡
流程图如下:
在这里插入图片描述
注意到在红黑树添加节点时,有两个细节:
1.首先是把新增的节点头插法设置成了 first首节点,这一步的目的我们可以先想一想这么做的原因,后续会讲到,这里先透个底:已添加的节点需要支持查找到,如果此时正在调整红黑树,我们不能使用二叉树二分查找法。详细的解答请看二叉树节点查找
2.调整树的平衡时有加锁,加锁的目的是什么,map.put()在添加节点时已经加了synchronized (f)使添加节点时同步操作,为什么这里还要加锁?加的是什么锁?我们来看一下

红黑树调整树平衡
加解锁

再次回顾下TreeBin数据结构红黑树占位节点TreeBin数据结构

/**
 * 获取红黑树重组的写锁
 */
private final void lockRoot() {
	//当前线程尝试加写锁,如果失败,进入自旋阶段contendedLock
	if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
		contendedLock(); // offload to separate method
}

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

/**
 * 自旋并尝试加锁
 * 写锁状态位 001,等待状态位 010,读锁状态位 100
 * 1.如果WRITER为空,则表明锁已释放,获取;
 * 2.否则,如果WAITER为空,则置WAITER位为1
 */
private final void contendedLock() {
	boolean waiting = false;
	for (int s;;) {
		/*
			WAITER : 0000 0010
			~WAITER : 按位取反,为 1111 1101
			s & ~WAITER = 0 : 说明 s只能是 000 或 010,即此时写锁(二进制第1位),读锁(二进制第3位)状态位都为 0;读锁写锁都没有,等待位不确定
		*/
		if (((s = lockState) & ~WAITER) == 0) {
			//尝试加写锁,加锁成功后,判断当前线程是否设置过等待标识,如果是,则去掉
			if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
				if (waiting)
					waiter = null;
				return;
			}
		}
		/*
			从上一个判断知道,走到该分支时s可能是 100或001,可能有读锁,可能有写锁
			s & WAITER(010) == 0 : 说明 s可能是 100或001,此时说明有读锁或者写锁
		*/
		else if ((s & WAITER) == 0) {
			/*
				s | WAITER:设置等待位为1,此时lockState可能为 110-读等待 或 011-写等待,设置成功则将当前线程置为等待线程
			*/
			if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
				waiting = true;
				waiter = Thread.currentThread();
			}
		}
		/*
			如果当前线程已经为等待状态,则将线程阻塞,等待其他线程唤醒它
			为何在此处将线程阻塞后,只会在TreeBin.find方法内进行unpark,唤醒线程
			原因我分析:
				1.首先在ConcurrentHashMap执行添加节点时,已经对要添加节点的hash槽进行了synchronize加锁操作,紧接着进入到红黑树添加节点需要重新调整红黑树时,再一次对
				树进行写锁尝试,如果 lockState:000 -> 001 失败后,进入到线程的自旋,而又因为上面已经对树进行了synchronize 互斥写操作,因此,同一个时刻lockState被占用的状态只可能是 010或者100,
				a) 如果是010有线程在等待获取锁时for循环内第一个if逻辑(读写锁都没有)就会进入分支并尝试重新加写锁(这里为什么可以直接加,因为这里不可能会出现同一时刻多个写锁等待的,所以只可能是因为在调整树平衡时有其他线程在二叉遍历查找节点,
				因此,只可能是获取写锁的线程在等待前面的读取操作完成后再开始调整树);
				
				b) 而如果是100读锁时会进入到for循环内的第二个if((s & WAITER) == 0),会加等待,
				最后读等待锁加成功后自旋走到了此处,挂起线程。
				
				最后得知,发生加写锁失败的情况只可能是当前hash槽的红黑树上正在被其他线程读取,加了读锁,因此,只需要在find方法处在读取数据完成后,对锁进行释放,并唤醒当前正在
				等待的线程。
		*/
		else if (waiting)
			LockSupport.park(this);
	}
}

根据上面的加解锁代码片段讲解,我们可以回答[为什么这里还要加锁?的问题],调整红黑树平衡时加锁:是为了等待前边正在进行二叉查找的线程执行完毕后才开始调整。
而如果红黑树正在被调整,我们又怎么保证可以获取到节点呢?咱们看看红黑树节点查找代码

红黑树查找节点
/**
 * Returns matching node or null if none. Tries to search
 * using tree comparisons from root, but continues linear
 * search when lock not available.
 * 注释已经说的很明白了:尝试使用红黑树的二叉搜索来寻找节点,如果锁不可用,则进行线性的迭代方式(链表遍历)来进行节点查找
 */
final Node<K,V> find(int h, Object k) {
	if (k != null) {
		for (Node<K,V> e = first; e != null; ) {//注意,这里是从first开始遍历
			int s; K ek;
			/*
				WAITER|WRITER : 011 表示有写锁和等待写锁
				lockState & 011 != 0 说明当前hash槽的红黑树有写锁001或者等待写锁010,红黑树正在被调整中,
				此时不能进行树的查找,直接进行链表的迭代查找
			*/
			if (((s = lockState) & (WAITER|WRITER)) != 0) {
				if (e.hash == h &&
					((ek = e.key) == k || (ek != null && k.equals(ek))))
					return e;
				e = e.next;
			}
			/*
				没有写锁和等待锁,则给红黑树加一把叠加的读锁(这里应该没什么疑问,如果当前树已经有线程正在读取,那么读叠加是可以的,因为写锁已经在等待了);接着开始进行红黑树搜索,读取完毕后释放读锁
			*/
			else if (U.compareAndSwapInt(this, LOCKSTATE, s,
										 s + READER)) {
				TreeNode<K,V> r, p;
				try {
					//红黑树节点查找,这里其实调用的是父类TreeNode.findTreeNode方法
					p = ((r = root) == null ? null :
						 r.findTreeNode(h, k, null));
				} finally {
					Thread w;
					/*
						读取完毕后,读锁个数减1,U.getAndAddInt()增加并返回旧值;当旧值=READER|WAITER时,说明此次减少后所有的读操作都已经完成,lockState此时最新的值应该变成了 010,这里的等待也只能是前面被挂起的写锁。
						唤醒当前正在等待的线程
					*/
					if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
						(READER|WAITER) && (w = waiter) != null)
						LockSupport.unpark(w);//唤醒正在等待的线程
				}
				return p;
			}
		}
	}
	return null;
}

看完红黑树查找代码后,我们也就又回到了前边【为什么红黑树新增节点时,先是头插法将新增的节点设置成了 first首节点?的问题】:因为TreeBin 内保存的first首节点被volatile修饰,在保证内存可见性的情况下,又被map.put() 的synchronized同步限制妥妥的保证原子性,因此,如果说红黑树正在被调整,进行线性链表迭代(从first开始)的时候,恰好也能迭代到刚刚添加进去的节点。

ConcurrentHashMap计数

ConcurrentHashMap1.8因为添加操作时锁粒度是一个个的hash槽,相互之间是隔离的,因此多线程进行节点添加的情况下,如果说每一个线程添加操作都直接操作 ConcurrentHashMap的计数字段(这里假设为count),那么多线程并发的情况下,难免会有线程竞争的问题,为了减少这些不必要的线程间竞争,争取更高的效率,大神又采用了神奇的计数法:当有多线程竞争时,采用分段计数法(参考的是LongAdder模式),然后再合并,无线程竞争是采用一般的计数法。

多线程计数
/**
 * A padded cell for distributing counts.  Adapted from LongAdder
 * and Striped64.  See their internal docs for explanation.
 * 多线程添加map节点时,用来分段技术的数据结构
 */
@sun.misc.Contended static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}
//分段计数完成后汇总
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;
}

/**
 * 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
 */
private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
   /*
    * (as = counterCells) != null 只有当发生多个线程竞争计数时不为空
    * baseCount:存放的是map节点数,当没有多线程竞争计数时,直接使用该字段累加,如果修改值失败,那么说明可能存在多个线程竞争
    */
    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) {
        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();
        }
    }
}

private final void fullAddCount(long x, boolean wasUncontended) {
    int h;
    if ((h = ThreadLocalRandom.getProbe()) == 0) {
        ThreadLocalRandom.localInit();      // force initialization
        h = ThreadLocalRandom.getProbe();
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        CounterCell[] as; CounterCell a; int n; long v;
        if ((as = counterCells) != null && (n = as.length) > 0) {
            if ((a = as[(n - 1) & h]) == null) {
                if (cellsBusy == 0) {            // Try to attach new Cell
                    CounterCell r = new CounterCell(x); // Optimistic create
                    if (cellsBusy == 0 &&
                        U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                        boolean created = false;
                        try {               // Recheck under lock
                            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;           // Slot is now non-empty
                    }
                }
                collide = false;
            }
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                break;
            else if (counterCells != as || n >= NCPU)
                collide = false;            // At max size or stale
            else if (!collide)
                collide = true;
            else if (cellsBusy == 0 &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                try {
                    if (counterCells == as) {// Expand table unless stale
                        CounterCell[] rs = new CounterCell[n << 1];
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        counterCells = rs;
                    }
                } finally {
                    cellsBusy = 0;
                }
                collide = false;
                continue;                   // Retry with expanded table
            }
            h = ThreadLocalRandom.advanceProbe(h);
        }
        else if (cellsBusy == 0 && counterCells == as &&
                 U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
            boolean init = false;
            try {                           // Initialize table
                if (counterCells == as) {
                    CounterCell[] rs = new CounterCell[2];
                    rs[h & 1] = new CounterCell(x);
                    counterCells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
            break;                          // Fall back on using base
    }
}
  • 11
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值