【源码阅读】ConcurrentHashMap 1.8

一、为什么引入 ConcurrentHashMap 1.8 ?

  • JDK 1.7 采用分段锁思想,整个 Hash 表被分成多个段,每个段中会对应一个 Segment 段锁,段与段之间可以并发访问,但是多线程想要操作同一个段是仍需要获取锁的。
  • JDK 1.8 在控制并发方面则取消了基于 Segment 的分段锁思想,改用 CAS + synchronized 控制并发操作;在底层数据结构使用 Node 数组+链表+红黑树,但为了兼容 jdk1.7,若仍保留了 segment 这个数据结构。

二、源码阅读

(1) 底层数据结构

成员变量定义了 ConcurrentHashMap 一些边界值

	// node数组最大容量:2^30=1073741824
	private static final int MAXIMUM_CAPACITY = 1 << 30;
	// 默认初始值,必须是2的幕数
	private static final int DEFAULT_CAPACITY = 16;
	//数组可能最大值,需要与toArray()相关方法关联
	static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	//并发级别,遗留下来的,为兼容以前的版本
	private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
	// 负载因子
	private static final float LOAD_FACTOR = 0.75f;
	// 链表转红黑树阀值,> 8 链表转换为红黑树
	static final int TREEIFY_THRESHOLD = 8;
	//树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
	static final int UNTREEIFY_THRESHOLD = 6;
	static final int MIN_TREEIFY_CAPACITY = 64;
	private static final int MIN_TRANSFER_STRIDE = 16;
	private static int RESIZE_STAMP_BITS = 16;
	// 2^15-1,help resize的最大线程数
	private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
	// 32-16=16,sizeCtl中记录size大小的偏移量
	private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
	// forwarding nodes的hash值
	static final int MOVED     = -1;
	// 树根节点的hash值
	static final int TREEBIN   = -2;
	// ReservationNode的hash值
	static final int RESERVED  = -3;
	// 可用处理器数量
	static final int NCPU = Runtime.getRuntime().availableProcessors();
	//存放node的数组
	transient volatile Node<K,V>[] table;
	/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
	 x = 0:默认值
	 x = -1:代表哈希表正在进行初始化
	 x < 0:相当于 HashMap 中的 threshold,表示阈值
	 x < -1:代表有多个线程正在进行扩容*/
	private transient volatile int sizeCtl;

再看一下底层数据结构 Node,它实现了 HashMap 中的 Entry,所以 Node 只不过是一个键值对。

static class Node<K,V> implements Map.Entry<K,V> {
    //链表的数据结构
    final int hash;
    final K key;
    //val和next都会在扩容时发生变化,所以加上volatile来保持可见性和禁止重排序
    volatile V val;
    volatile Node<K,V> next;
    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; }
    //不允许更新value 
    public final V setValue(V value) {
        throw new UnsupportedOperationException();
    }
    public final boolean equals(Object o) {
        Object k, v, u; Map.Entry<?,?> e;
        return ((o instanceof Map.Entry) &&
                (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                (v = e.getValue()) != null &&
                (k == key || k.equals(key)) &&
                (v == (u = val) || v.equals(u)));
    }
    //用于map中的 get() 方法,子类重写
    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 继承了 Node

static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;

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

TreeBin 用作树的头结点,只存储 root 和 first 节点,不存储节点的key、value值。

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

ForwardingNode 在数据转移的时候放在头部的节点,是一个空节点。

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

(2) 构造方法

public ConcurrentHashMap(int initialCapacity) {
	if (initialCapacity < 0)	 // 初始容量小于0,抛出异常
		throw new IllegalArgumentException();
	int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
			   MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); // 找到最接近该容量的2的幂次方数
	this.sizeCtl = cap;
}

public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
	this.sizeCtl = DEFAULT_CAPACITY;
	putAll(m);	// 将集合m的元素全部放入
}

public ConcurrentHashMap(int initialCapacity, float loadFactor) {
	this(initialCapacity, loadFactor, 1);
}

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

观察最后一个构造方法发现新增了一个 sizeCtl 变量,它是怎么得来的呢?

总结一下:

  • 该初始化过程通过指定的初始容量 initialCapacity,加载因子 loadFactor 和预估并发度concurrencyLevel 三个参数计算出一个最小的且大于等于 initialCapacity 大小的 2 的 n 次幂数,即 table 数组的初始大小 sizeCtl
  • 若 initialCapacity 为 15,则 sizeCtl 为 16
  • 但如果 initialCapacity 大小超过了允许的最大值,则 sizeCtl 为最大值

(3) put 方法

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

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) 
	throw new NullPointerException();
    int hash = spread(key.hashCode());	// 得到 hash 值
    int binCount = 0;			// 用于记录相应链表的长度
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 如果数组"空",进行 Node 数组初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 通过 hash 值算出对应的数组下标,从内存中得到第一个节点 f,并检查是否为空 ,如果是再用cas赋值
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 用一次 CAS 操作尝试将新值 f 放入位置 i
            // 如果 CAS 失败,说明此时有并发操作使得该位置不为空,那么直接进入下一次循环 
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break;
        }
        // 走到这里,f 就不为空了;如果整个Map正在扩容
        else if ((fh = f.hash) == MOVED)
            // 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了
            tab = helpTransfer(tab, f);
        else { // 到这里就是说,f 是该位置的非空头结点
            V oldVal = null;
            // 获取数组该位置的头结点的同步锁
            synchronized (f) {
                if (tabAt(tab, i) == f) {//因为在获取锁的过程中,可能被其他线程改变,所以再次检查是否等于原值
                    if (fh >= 0) { 	// 头结点的 hash 值大于 0,说明是链表
                        binCount = 1; 	// 记录链表的长度
                        // 遍历链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果发生hash冲突且遇到"相等"的 key,则进行值覆盖
                            if (e.hash == hash &&
                                  ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            // 到了链表的最末端,将这个新值放到链表的最后面
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value, null);
                                break;
                            }
                        }
                    } // 红黑树,这里将 TreenNode 改为 TreeBin 其实是为了配合 Synchronize,原因如下:
		    //如果此处的结点为红黑树,如果按照 HashMap 的方式去判断并插入,会导致此处的头结点会发生变化,而变化之后锁住的对象就不是根节点了。
		    //而如果此处是一个 TreeBin,线程修改的只是 TreeBin 里面的红黑树,无论里面的树怎么改变,此处的锁亦然不变。
		    //这种 synchronized (f) {...},写法在 put 方法中也可见(f是TreeBin类型)
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }//元素个数
            if (binCount != 0) {
                // 判断链表长度是否 > 8
                if (binCount >= TREEIFY_THRESHOLD)
                    // 如果当前数组的总元素 < 64,那么会选择进行数组扩容,而不是转换为红黑树
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

put 方法的篇幅较长,总结一下:

  1. 根据 key 的 hashcode 值算出 hash 值,遍历内部的 table,并判断 tab 是否为空:
    1. 如果 tab 为空,则对 tab 进行初始化
    2. tab 不空则根据 hash 值得到 tab 的下标 i,拿到获得该位置头结点 f,判断 f 是否为空:
      • 如果 cas 检查到 f 为空,则将新对象放到位置 i,并结束 tab 的遍历。
  2. 如果 f.hash == MOVED 则表示 此时数组正在扩容,则会去尝试协助其他线程扩容。
  3. 如果 f 不空,则尝试获取结点 f 的同步锁:
    1. 如果拿到锁,就判断 f 的类型:
      1. 如果是链表,检查是否有 hash 冲突和 key 相同的情况,如果 onlyIfAbsent = false 则将旧值进行覆盖;如果没有冲突则使用尾插法将结点插入。
      2. 如果是红黑树,则执行红黑树的插入方法。
  4. 最后添加结点成功就调用 addCount 方法统计 size,并且在 addCount 方法中检查是否需要扩容。
  • Q:那 put 方法是在哪些方面提供了线程安全保障的呢?如果怎么保证线程的呢?
  • A:时机有:初始化数组时、添加结点是、以及扩容时。

3.1 initTable 方法

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        // thread2在执行下面的compareAndSwapInt方法cas不成功,那么thread2会到这个分支
        // 从而交出 CPU 等待下次系统调度
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        // 一定是只有一个线程CAS成功,如果当前线程可以将 sizeCtl 设置为 -1,代表抢到了机会
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {//有可能在这里已经别其他线程初始化成功,所以再次判断一下
                if ((tab = table) == null || tab.length == 0) {
                    // DEFAULT_CAPACITY 默认初始容量是 16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 初始化数组,长度为 16 或初始化时提供的长度
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 将这个数组赋值给 table,table 是 volatile 的
                    table = tab = nt;
                    // 如果 n 为 16 的话,那么这里 sc = 12;n - n/4 = 0.75 × n
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;// 设置 sizeCtl 为 sc,我们就当是 12 吧
            }
            break;
        }
    }
    return tab;
}

初始化方法不难,也总结一下:

  1. 如果 tab 不空就直接返回。
  2. 如果 tab 为空,每个并发线程就使用 cas 去竞争初始化机会:
    1. 如果某一个线程竞争不到机会,就会放弃竞争,自旋至 tab 不空为止。
    2. 否则,就对 tab 进行初始化。

注: sizeCtl 默认为 0,sizeCtl 中记录 size 大小的偏移量,用来控制 table 的初始化和扩容操作.它的数值有以下含义:

  • -1:代表 table 正在初始化,其他线程应该交出 CPU 时间片,退出
  • -N: 表示正有 N-1 个线程执行扩容操作
  • >0:表示 tab 已经初始化,代表 tab 容量,默认为 tab 大小的 0.75 倍,如果还未初始化,代表需要初始化的大小。

按照代码的执行顺序,如果没有初始化,那么可能就会进行扩容 transfer,下面就来聊一下扩容...


我们知道在 jdk1.7 中,最大并发量就是就是 seg 的个数,在存在并发操作时,虽然这样设计使得 seg 对象在扩容时不会影响到其他 seg 对象,但如果该 seg 正在扩容,其他线程还得等到扩容完毕才能对 seg 对象进行读写,因此扩容效率就成为了并发的一个瓶颈;

jdk1.8 就对这个问题进行了一个优化:首先 JDK1.8 去掉了分段锁,将锁的级别控制在了更细粒度的 Node 元素级别,同时作者 Doug lea 大神就认为既然其他线程闲着也是闲着,不如一起参与扩容吧,于是在 jdk1.8 中就引入了一个 ForwardingNode 类以及一个 sizeCtl 来控制 table 的初始化和扩容操

3.2 helpTransfer 方法

我们在 put 方法中发现当 (fh = f.hash) == MOVED 成立时,会进入 helpTransfer 方法中。里面涉及到了一个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;
    }
    ...
}

字面意思是:当整个 table 的某个位置正在进行扩容时,会把一个 ForwardingNode 类型的结点插在 table 的某个 tab[i] 的头部,注意:并不是某个位置有 ForwardingNode 就表示扩容完全结束了,其他位置可能没有。

作用不难想到

  • 一个用于连接两个 table 的节点类。它包含一个 nextTable 指针,用于指向下一张表。而且这个节点的 key value next 指针全部为n ull,它的 hash 值为 -1
  • 在扩容时,线程 t1 会先判断该槽点是否为空:
    • 如果为空,t1 就会将此处的第一个设置为 forwordingNode,告诉其他线程此位置的数据迁移已有线程包办。
    • 不为空,则采用头插法先把此位置的数据给迁移到新的数组中,最后给旧 table 的原位置赋值为 fwd。

helptransfer参考:https://www.jianshu.com/p/39b747c99d32

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 如果 tab 不空并且结点 f 不是ForwardingNode类型,说明这个位置不在扩容(其他位置可能在扩容)。
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { 
        int rs = resizeStamp(tab.length);
        //若sizeCtl是负数,tab、nextTable和当前table、nextTable相同,说明扩容尚未完成
        //因为一旦扩容完成,就会将cmap的属性更新
        while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { 
            // 1.如果sizeCtl>>>16 != rs,则表示标识符变化了
            // 2.或者sizeCtl == rs + 1(扩容结束了,不再有线程进行扩容)(默认第一个线程设置sc == (rs左移16位+2),
                // 当第一个线程结束扩容,会将 sc 减一。此时sc=rs+1)
            // 3.或者sizeCtl == rs + 65535  (如果达到最大帮助线程的数量,即 65535)
            // 4.或者transferIndex正在调整 (扩容结束)
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0) 
                break;
            /* sc的含义
            -1:代表 table 正在初始化,其他线程应该交出 CPU 时间片,退出
            -N:表示正有 N-1 个线程执行扩容操作
            >0:表示 tab 已经初始化,代表 tab 容量,默认为 tab 大小的 0.75,如果还未初始化,代表需要初始化的大小。*/
            // 用CAS尝试将SIZECTL加1,表示表示增加了一个线程协助扩容
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

总结一下 helpTransfer 方法:

  1. 先判断当前位置的结点类型是否是 ForwardingNode,和当前结点的 nextTable 是否为空:
  2. 如果 f 是 ForwardingNode 类型并且 f 的 nextTable 属性为空,则表示 map 不在扩容,返回一个 table 即可(可能是旧数组,也可能是新数组)
  3. 否则,进一步判断 this.nextTable 是否和 f.nextTab 相等,以及 sc=sizeCtl 是否小于 0:
    • 如果 this.nextTable==f.nextTab,并且 sc < 0,则表示旧数组正在扩容,为了保险,再次检查 4 种代表扩容是否结束的情况是否符合:
      • 如果扩容结束,break 掉 while 返回即可
      • 否则,再次判段 sc 是否与内存中的 SIZECTL 相同:
        • 如果相同,则将用 cas 尝试将 SIZECTL 加 1,表示表示增加了一个线程协助扩容,然后进入 transfer 方法。
        • 否则,回到 while 中再次检查 this.nextTable==f.nextTab 是否成立,自旋。

存疑:

  • Q1:在总结的时候,我有一个疑惑,为什么一定要用 cas 检查 SIZECTL,才能进入 transfer 方法呢?
    • A1:
  • Q2:为什么当 sc == rs + 1 就表示 cmap 扩容成功?
    • A2:这个判断可以在 addCount 方法中找到答案:默认第一个线程设置 sc == (rs >>> 16) + 2,当第一个线程结束扩容了,就会将 sc--。此时 sc = rs + 1

如果上面的逻辑都没问题,接下来就应该看看 cmap 是如何数据迁移的...


3.3 transfer 方法

数据迁移的逻辑比较复杂,因为它支持并发扩容,而且还没有加锁。详细看看 transfer 方法...

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // stride(步长)在单核下直接等于n,多核模式下为 (n>>>3)/NCPU,最小值是16
    // 一共会有n个位置是需要进行迁移
    // n个位置分为多个区域,每个区域有 stride 个任务
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    // 如果 nextTab 为 null,先进行一次初始化
    // 保证只有第一个发起迁移的线程调用此方法时nextTab为null
    if (nextTab == null) {
        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;//ConcurrentHashMap 中的属性
        transferIndex = n;  //同上,用于控制迁移的位置,初始位置为n
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // advance 指的是做完一个位置的迁移工作,可以准备做下一个位置的
    boolean advance = true;    //控制是否要继续向前扫描
    boolean finishing = false; //确保在提交NextTab之前进行扫描,to ensure sweep before committing nextTab
    // i是当前扫描位置索引,bound是左边界,注意是从后往前
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // advance为true表示需要继续向区域[bound, transferIndex]扫描
        // 简单理解结局:i指向transferIndex,bound指向 transferIndex-stride
        while (advance) {
            int nextIndex, nextBound;
            // 如果--i>=bound证明当前扫描区域还没有扫描完,所以没必要扩张区域。第一次碰到不会执行(i初始为0)
            if (--i >= bound || finishing) 
                advance = false;
            // 如果transferIndex<=0,说明旧数组的所有位置都有线程处理
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;    //退出循环
            }//如果cas成功则表示当前线程获得了一个扫描区域
            else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
                        nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;  //nextBound 是这次迁移任务的边界
                i = nextIndex - 1; 
                advance = false;   //当前线程已确定扫描区域
            }
        }//当前线程t进到这里,表示t"没事可干"
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;             // sizeCtl的别名
            if (finishing) {    // 如果table的迁移操作已经完成
                nextTable = null;
                table = nextTab; // 将新的nextTab赋值给table,完成迁移
                // 重新计算 sizeCtl:n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75倍(4)
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            // 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
            // 然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
            // 这里使用CAS操作对sizeCtl进行减1,代表做完了属于自己的任务
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 任务结束,方法退出
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                // 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
                // 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        // 如果位置i为空,就用cas插入一个ForwardingNode结点
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 该位置处是一个ForwardingNode(MOVED的本质),代表该位置数据迁移完毕
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {// 获取锁并开始处理数组该位置处的迁移工作
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 头结点的hash值>0,表示此处为链表
                    if (fh >= 0) {
                        // 将链表一分为二,
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 低位链表放在新数组的位置i
                        setTabAt(nextTab, i, ln);
                        // 高位链表放在新数组的位置i+n
                        setTabAt(nextTab, i + n, hn);
                        // 将原数组该位置处设置为fwd,标记该位置已经处理完毕,
                        setTabAt(tab, i, fwd);
                        // advance 设置为 true,标记该位置已经迁移完毕
                        advance = true;
                    } // 红黑树的迁移
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            } else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        // 如果低位链表的节点数少于 8,则将此处红黑树拆成链表
                        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 放置在新数组的位置 i
                        setTabAt(nextTab, i, ln);
                        // 将 hn 放置在新数组的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
                        // 其他线程看到该位置的 hash 值为 MOVED,则不进行迁移
                        setTabAt(tab, i, fwd);
                        // advance 设置为 true,代表该位置已经迁移完毕
                        advance = true; //继续向前扫描
                    }
                }
            }
        }
    }
}

整理一下思路:视频 85:00

  1. ConcurrentHashMap 的扩容支持多线程,在多线程环境下,每一个线程会通过步长计算出自己的负责数据迁移的区域。
  2. 当线程在某个位置 i 迁移数据时,每一个线程都会锁住 i 位置的头结点,这样保证了被锁的位置不能被其他线程操作。
  3. 当位置 i 的数据被一个线程迁移后,该位置的锁将会别释放掉,并将此处的头结点设置为 ForwardingNode,告诉其他线程不必在继续协助该位置,但没有 ForwardingNode 的结点可以进行 put 操作,最后的数据还是会搬到新数组中, 与此同时,刚刚的线程继续从右往左扫描,尝试搬迁其他位置的数据:
    1. 如果有尚未搬迁而且未被其他线程正在搬迁的位置,该线程将会继续变迁该位置的数据。
    2. 否则,该线程将会退出本次 transfer 的流程,继续等待其他线程完成数据迁移。
  4. 所有数据迁移完全后,扩容完成。

3.3 treeifyBin 方法

如果要进入 treeifyBin 方法后判断是否要进行树化

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        // MIN_TREEIFY_CAPACITY = 64
        // 如果数组长度小于 64 的时候,其实也就是 8 < x < 64 时会对数组扩容
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            // 后面我们再详细分析这个方法
            tryPresize(n << 1);
        // 如果 b 是非空头结点
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    // 下面就是遍历链表,建立一颗红黑树
                    TreeNode<K,V> hd = null, tl = null;
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null);
                        if ((p.prev = tl) == null) hd = p;
                        else			   tl.next = p;
                        tl = p;
                    }
                    // 将红黑树设置到数组相应位置中
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

3.4 addCount 方法

addCount 方法不容易理解,简单说一下它的作用吧:

  • 对 table 的总元素个数加一。无论是通过修改 baseCount,还是通过使用 CounterCell。当 CounterCell 被初始化了,就优先使用 CounterCell,不再使用 baseCount。
  • 检查是否需要扩容,或者是否正在扩容。如果需要扩容,就调用扩容方法,如果正在扩容,就帮助其扩容。

对应视频 JDK8中ConcurrentHashMap源码解析(上) 的 65分钟

(4) get 方法

get 方法相对比较简单

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //计算hash值
    int h = spread(key.hashCode());
    //根据hash值确定节点位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        //如果搜索到的节点key与传入的key相同且不为null,直接返回这个节点	
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //如果eh<0 说明这个节点在树上 直接寻找
        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;
}

简单总结一下:

  • 计算 key 的 hash 值,然后再算出桶下标 i
  • 判断 i 位置是否有元素:
    • 如果没有元素,返回 null
    • 否则,如果 hash 值与 key 都相同,那么返回当前位置元素的 val
  • 如果 hash 值与 key 都不相同,那么在此检查位置 i 的结点类型:
    • 如果是红黑树就调用红黑树的查找方法
    • 如果是链表就遍历链表找出对应元素。

(5) size 方法




详讲每个方法,包括 UNsafe:https://blog.csdn.net/u010723709/article/details/48007881

addCount:https://www.jianshu.com/p/749d1b8db066 https://www.cnblogs.com/dgutfly/p/11425599.html


开源中国:https://my.oschina.net/hosee/blog/675884#h2_10 https://www.javadoop.com/post/hashmap#toc_11

https://blog.csdn.net/Bill_Xiang_/article/details/81122044

有目录那个博客园:https://www.cnblogs.com/study-everyday/p/6430462.html#autoid-2-1-4

扩容时如何读写?https://www.baidu.com/link?url=d0a2BxAnZnDwfy4sDAs7fphVkYDWcIP_52yCGvpjL-1JS14N25ioMV6irOKTJfDXPqIITojxHNu5-oo6TdT7_oJ6C9Epw2uDavBj2VKhw3a&wd=&eqid=b18494ab000c3aad000000065ecb71f4

helptransfer、以及sizeCtl 参考:https://www.jianshu.com/p/39b747c99d32

代码很多注释:https://www.cnblogs.com/zerotomax/p/8687425.html#go2

讲了finnish的怎么得到:https://www.cnblogs.com/yangming1996/p/8031199.html

折叠方法:https://www.cnblogs.com/leesf456/p/5453341.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章