ConcurrentHashMap学习笔记

参考:Java多线程进阶(二三)—— J.U.C之collections框架:ConcurrentHashMap结构

JDK版本:AdoptOpenJDK 11.0.10+9

本文是在学习了 这篇博文 之后整理的相关知识点,这篇博文已经讲解的相当好了。

1 基本概念

ConcurrentHashMap(发音:肯卡润特哈希迈普)是1.5的时候引入的,是一个线程安全的HashMap

ConcurrentHashMap的结果非常复杂,其中有一些基本结构需要好好理解。

在这里插入图片描述

1.1 桶

ConcurrentHashMap内部维护了一个Node类型的数组table

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

数组的每一个位置table[i]代表了一个

当插入键值对的时候,会根据keyhash值计算出对应的桶的位置,将value加入到对应的桶里面。

table一共可以包含4种不同类型的桶,都是Node或其子类:

  • Node类型
  • TreeBin类型,连接的是一颗红黑树,树节点类型为TreeNode
  • ForwardingNode类型
  • ReservationNode类型

1.2 Node节点

Node节点是ConcurrentHashMap中最基本的节点,是其他类型节点的父节点。

默认链接上table[i]桶上的节点就是Node节点。

当出现hash冲突的时候,Node节点会首先以 链表 的方式链接到table[i]上;

当节点超过一定数量的时候,链表会转化为红黑树。

下面是Node的源码:

    /**
     * 一个Node节点保存一个“键值对”,在链表结构中使用
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        // 哈希值
        final int hash;
        // 键
        final K key;
        // 值
        volatile V val;
        // 链表的指针,指向下一个Node节点
        volatile Node<K,V> next;

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

        Node(int hash, K key, V val, Node<K,V> next) {
            this(hash, key, 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 Helpers.mapEntryToString(key, val);
        }
        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)));
        }

        /**
         * Virtualized support for map.get(); overridden in subclasses.
         */
        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;
        }
    }

1.3 一些常量

ConcurrentHashMap内部定义了一些常量。

    /**
     * 最大的容量,也就是2^30。
     * (在ConcurrentHashMap中,容量定义为2的幂,
     * 而Integer最大值为2^31 - 1,所以容量的最大值只能取到2^30)
     */
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认初始容量,为16
     */
    private static final int DEFAULT_CAPACITY = 16;

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

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

    /**
     * 负载因子,默认为0.75,为了兼容1.8以前的版本而留下的
     */
    private static final float LOAD_FACTOR = 0.75f;

    /**
     * 链表转化为树的阈值。链表上Node节点大于等于8个的时候,链表将会转化为树。
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 树转化为链表的阈值。树上节点小于等于6个的时候,树转化为链表。
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 在链表转化为树之前,还会有一次判断:
     * 只有table数组的长度大于64,才会发生转化。
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * 在树转变为链表之前还会有一次判断:
     * 只有键值对的数量小于16,才会发生转换。
     */
    private static final int MIN_TRANSFER_STRIDE = 16;

    /**
     * 用于在扩容时生成唯一的随机数
     */
    private static final int RESIZE_STAMP_BITS = 16;

    /**
     * 可以同时进行扩容操作的最大线程数
     */
    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;

    /*
     * Encodings for Node hash fields. See above for explanation.
     */
    static final int MOVED     = -1; // 标识ForwardingNode节点
    static final int TREEBIN   = -2; // 标识红黑树节点
    static final int RESERVED  = -3; // 标识ReservationNode节点
    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

    /** CPU核心数,扩容时使用 */
    static final int NCPU = Runtime.getRuntime().availableProcessors();

1.4 一些字段

ConcurrentHashMap中定义了一些字段。

    /**
     * Node数组,代表整个Map,在首次插入键值对的时候创建,大小总是2的幂。
     */
    transient volatile Node<K,V>[] table;

    /**
     * 扩容后的新table数组,只有在扩容的时候才使用
     */
    private transient volatile Node<K,V>[] nextTable;

    /**
     * 计数基数,当没有线程竞争的时候,计数将加到这个变量上
     */
    private transient volatile long baseCount;

    /**
     * 控制table的初始化和扩容:
     *   0 : 初始化的默认值;
     *  -1 :表示有线程正在进行table的初始化;
     * > 0 :表示table初始化时使用的容量,或者初始化/扩容完成之后的threshold;
     * -(1 + nThreads):表示正在执行扩容任务的线程数;
     */
    private transient volatile int sizeCtl;

    /**
     * 扩容时需要用到的一个下标变量
     */
    private transient volatile int transferIndex;

    /**
     * 自旋标识位,用于CounterCell[]扩容时使用
     */
    private transient volatile int cellsBusy;

    /**
     * 计数数组,出现并发冲突的时候使用
     */
    private transient volatile CounterCell[] counterCells;

    // 试图字段
    private transient KeySetView<K,V> keySet;
    private transient ValuesView<K,V> values;
    private transient EntrySetView<K,V> entrySet;

2 构造函数

ConcurrentHashMap提供了5个构造函数。

ConcurrentHashMap采用 “懒加载” 模式,只有到首次插入键值对的时候,才会真正的初始化table数组。

2.1 空构造函数

    public ConcurrentHashMap() {
    }

只创建ConcurrentHashMap对象,不做任何初始化。

2.2 指定table初始容量、负载因子、并发级别的构造函数

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

注意:concurrencyLevel只是为了兼容1.8以前的版本,并不是实际的并发级别,loadFactor也不是实际的负载因子,这两个都失去了原来的意义,仅仅对初始化容量有一定的控制作用。

2.3 指定table初始容量的构造函数

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

负载因子默认使用的是常量LOAD_FACTOR,值为0.75。并发级别默认为1

2.4 指定table初始容量、负载因子的构造函数

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

这里,默认并发级别是1

2.5 根据已有的map构造

    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }

这里使用的初始化容量为DEFAULT_CAPACITY,值为16。然后保存所有的键值对。

3 重要的方法

下面对ConcurrentHashMap中最终要的也是最常用的几个方法进行分析:

  • put操作
  • get操作
  • size计算集合大小

3.1 put插入键值对

put插入一个键值对,keyvalue均不能为null

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

实际调用的时putVal(key, value, false)函数:

    /**
     * onlyIfAbsent == true,仅当key不存在的时候才会插入键值对
     */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key和value不能为null
        if (key == null || value == null) throw new NullPointerException();
        // 计算出key对应的hash值
        int hash = spread(key.hashCode());
        
        /**
         * 使用链表保存时,binCount记录table[i]这个桶中所保存的结点数;
         * 使用红黑树保存时,binCount == 2,保证put后更改计数值时能够进行扩容检查,同时不触发红黑树化操作
         */
        int binCount = 0;
        
        // 自旋插入结点,直到成功
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh; K fk; V fv;
            
            // CASE1: 首次插入键值对时,需要初始化table —— 懒加载
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            
            // CASE2: table[i]对应的桶为null
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 插入一个链表结点
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                    break;
            }
            
            // CASE3: 发现是ForwardingNode结点,说明此时table正在扩容,则尝试协助数据迁移
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null)
                return fv;
            // CASE4: 出现hash冲突,也就是table[i]桶中已经曾经添加了Node节点
            else {
                V oldVal = null;
                // 锁住table[i]结点
                synchronized (f) {
                    // 再判断一下table[i]是不是第一个结点, 防止其它线程的写修改
                    if (tabAt(tab, i) == f) {
                        
                        // CASE4.1: table[i]是链表结点
                        if (fh >= 0) {
                            binCount = 1;
                            // 找到“相等”的结点,判断是否需要更新value值
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                // 采用“尾插法”插入新结点
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key, value);
                                    break;
                                }
                            }
                        }
                        
                        // CASE4.2: table[i]是红黑树结点
                        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;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                
                // 如果链表中节点个数达到阈值,链表转化为红黑树
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 计数值加1
        addCount(1L, binCount);
        return null;
    }

put添加键值对的大致流程描述如下:

  1. 首先计算出keyhash值,然后根据i = (n - 1) & hash找到table数组中桶的位置i
  2. 如果是第一次插入,需要对table初始化;
  3. 如果table[i]null,那么直接通过CAS插入一个新的Nodetable[i]桶下面;
  4. 如果发现table[i]ForwardingNode节点,说明此时table正在扩容,则尝试协助数据迁移;
  5. 如果发生了hash冲突,说明table[i]桶中已经曾经添加过Node节点,需要用synchronized锁住table[i]节点,并开始解决冲突问题。如果table[i]下面是链表,那么从第一个节点开始遍历,如果找到了和插入节点“相等”的节点,那么根据onlyIfAbsent的值决定是否需要更新节点的value;如果遍历到了尾节点,还没有找到“相等”的节点,那么采用“尾插法”在链表的尾部添加新的Node节点。如果table[i]下面是红黑树,调用putTreeVal方法将节点添加到树中。
  6. 完事了,要看一下table[i]下面链表中的节点个数是否超过了阈值(8个),如果超过了,就要将链表转化为红黑树。

3.1.1 初始化table数组

注意,在首次插入键值对时,需要初始化table —— 懒加载。

    /**
     * 初始化table,使用sizeCtl作为初始化容量。
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        // 自旋,直到初始化成功
        while ((tab = table) == null || tab.length == 0) {
            // 如果sizeCtl小于0, 说明table已经正在初始化或者扩容中
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            
            // 通过CAS将sizeCtl值更新为-1,表示table正在初始化
            else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        // 这里实际上是0.75*n
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

初始化table数组的时候,会将sizeCtl作为table数组的大小,这个值是0.75*n,也就是3/4的数组大小,这样相当于设置了一个阈值。

3.1.2 链表转红黑树

当链表中的节点个数达到阈值(8个),会将链表转化为红黑树:

    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n;
        if (tab != null) {
            // CASE 1: table的大小 < MIN_TREEIFY_CAPACITY(64)时,直接进行table扩容,不进行红黑树转换
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            
            // CASE 2: table的大小 ≥ MIN_TREEIFY_CAPACITY(64)时,进行链表 -> 红黑树的转换
            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;
                        }
                        // 以TreeBin类型包装,并链接到table[index]中
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

3.2 get获取value

get通过key来获取对应的value,找不到则返回null

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        
        // 计算key的hash值
        int h = spread(key.hashCode());
        
        // 根据i = (n - 1) & hash找到table[i]桶位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            // CASE 1: 如果table[i]这个节点就是要找的数据,则直接返回value
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            
            // CASE 2: 如果table[i]的节点的hash值小于0, 说明遇到了特殊节点,调用find方法查找
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            
            // CASE 3: 如果是链表,按链表的方法查找,从第一个节点遍历,找到就返回value
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

get获取数据的大致流程描述如下:

  1. 首先计算keyhash值,然后根据i = (n - 1) & hash找到table数组中桶的位置i
  2. 如果table[i]key和待查找的key相同,直接返回table[i]这个节点的value
  3. 如果table[i]对应的节点是特殊节点(hash 值小于0),则通过find方法查找节点;
  4. 如果table[i]对应的节点是普通的链表节点,则按照链表的方式从头遍历。

3.2.1 find查找节点

CASE 2的情况下,如果table[i]是特殊节点,那么需要调用对应节点的find方法来查找。

  1. Node节点的find
        /**
         * Node节点的find,实际上是链表的查找
         */
        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;
        }

table[i]节点是Node节点的时候,说明是链表结构,那么直接从表头开始遍历查找。

  1. TreeBin节点的find

TreeBin的查找比较特殊,我们知道当槽 table[i]TreeBin结点占用时,说明链接的是一棵红黑树。由于红黑树的插入、删除会涉及整个结构的调整,所以通常存在读写并发操作的时候,是需要加锁的。

ConcurrentHashMap采用了一种 类似读写锁 的方式:当线程持有写锁(修改红黑树)时,如果读线程需要查找,不会像传统的读写锁那样阻塞等待,而是转而以链表的形式进行查找(TreeBin本身是Node类型的子类,拥有Node的所有字段)。

        final Node<K,V> find(int h, Object k) {
            if (k != null) {
                for (Node<K,V> e = first; e != null; ) {
                    int s; K ek;
                    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.compareAndSetInt(this, LOCKSTATE, s,
                                                 s + READER)) {
                        TreeNode<K,V> r, p;
                        try {
                            p = ((r = root) == null ? null :
                                 r.findTreeNode(h, k, null));
                        } finally {
                            Thread w;
                            if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
                                (READER|WAITER) && (w = waiter) != null)
                                LockSupport.unpark(w);
                        }
                        return p;
                    }
                }
            }
            return null;
        }
  1. ForwardingNode节点的find

ForwardingNode是一种临时结点,在扩容进行中才会出现,所以查找也在 扩容的table 上进行。

  1. ReservationNode结点的find

ReservationNode是保留节点,不保存实际数据,直接返回null

3.3 size计算大小

调用ConcurrentHashMapsize()函数,可以获得键值对的个数:

    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }

实际上,调用的是sumCount()函数:

    final long sumCount() {
        CounterCell[] cs = counterCells;
        long sum = baseCount;
        if (cs != null) {
            for (CounterCell c : cs)
                if (c != null)
                    sum += c.value;
        }
        return sum;
    }

可以看到,sum的值分两部分:

  • baseCount的值
  • 如果CounterCell[]数组中每个对象的value的总和

公式如下:
s u m = b a s e C o u n t + ∑ i = 0 n C o u n t e r C e l l [ i ] sum = baseCount + \sum_{i=0}^{n}{CounterCell[i]} sum=baseCount+i=0nCounterCell[i]

3.3.1 计数原理

ConcurrentHashMap中定义了几个和计数相关的字段:

    /**
     * 计数基数,当没有线程竞争的时候,计数将加到这个变量上
     */
    private transient volatile long baseCount;

    /**
     * 自旋标识位,用于CounterCell[]扩容时使用
     */
    private transient volatile int cellsBusy;

    /**
     * 计数数组,出现多线程并发冲突的时候使用
     */
    private transient volatile CounterCell[] counterCells;

其中,在没发生多线程并发冲突的时候,计数将会加到baseCount上;当发生并发冲突的时候,计数将会加到counterCells数组上。

CounterCell的结构如下,其内部只有一个value属性,并且用volatile修饰:

    @jdk.internal.vm.annotation.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }

回归之前在put键值对的时候,在putVal函数的最后,调用了addCount(1L, binCount)将计数值加1,源码如下:

    private final void addCount(long x, int check) {
        CounterCell[] cs; long b, s;
        
        // 如果counterCells是null,说明之前一直没有发生并发冲突,那么直接将值累加到baseCount上
        if ((cs = counterCells) != null ||
            !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            // 否则,那就是曾经发生过并发冲突,那么将值累加到对应的CounterCell槽
            CounterCell c; long v; int m;
            boolean uncontended = true;
            
            // cs[ThreadLocalRandom.getProbe() & m])这里是根据线程的hash计算槽的位置,
            // 通过compareAndSetLong更新槽的值
            if (cs == null || (m = cs.length - 1) < 0 ||
                (c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
                // 如果更新槽的值失败,说明槽中也出现了并发冲突,
                // 可能涉及槽数组counterCells的扩容,所以调用fullAddCount方法
                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.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSetInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

4 扩容和数据迁移

前面的源码中提到,当table[i]桶中Node节点个数大于等于TREEIFY_THRESHOLD8个)的时候,将通过调用treeifyBin函数将链表转化为树:

    if (binCount != 0) {
        if (binCount >= TREEIFY_THRESHOLD)
            // 链表转化为树
            treeifyBin(tab, i);
        if (oldVal != null)
            return oldVal;
        break;
    }

但是这个转化不一定真的执行,查看treeifyBin源码:

    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n;
        if (tab != null) {
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            ......
        }
    }

可以看到,又进行了一次判断:如果table数组的长度小于MIN_TREEIFY_CAPACITY(值为64),调用tryPresize(n << 1)进行一次扩容,将table的长度扩大为原先的2倍。

4.1 扩容的基本思路

ConcurrentHashMap中,扩容分为两步:

4.1.1 table数组的扩容

一般是新建一个2倍大小的新table数组,这个过程由一个单线程完成,不允许出现并发。

4.1.2 数据迁移

数据迁移就是把旧table中的数据重新分配到新的table中。

因为节点的i = (n - 1) & hash,新的数组大小n变了,所以这一过程涉及到每个Node节点重新计算桶位置i

ConcurrentHashMap在处理数据迁移的时候,并不会重新计算每个key的桶位置,而是利用一种很巧妙的方法。

因为ConcurrentHashMap中规定table数组的的大小必须得是2的幂。

table扩容后(大小为原来的2倍),通过i = (n - 1) & hash计算出来的新的i值,要么等于原来的i的值,要么等于i + n

也就是说,旧数组中table[i]下的节点,一部分会被分配到新数组的table[i]下面,一部分会被分配到新数组的table[i+n]下面。这样就可以用“分治”的方法,可以多线程同时处理数据迁移。

4.2 扩容源码分析

下面来看看扩容的具体实现,查看tryPresize函数的源码:

    /**
     * 尝试预调整table数组的大小为指定的size大小
     */
    private final void tryPresize(int size) {
        // 视情况将size调整为2的幂
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            
            // CASE 1: table数组还没初始化,则先进行初始化
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            // 初始化table
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            
            // CASE 2: C <= SC, 说明已经被扩容过了,n >= MAXIMUM_CAPACITY说明数组已经达到最大容量
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            
            // CASE 3: 进行table数组扩容
            else if (tab == table) {
                // 根据容量n生成一个随机数,唯一标识本次扩容操作
                int rs = resizeStamp(n);
                // 这个CAS操作可以保证,仅有一个线程会执行扩容
                if (U.compareAndSetInt(this, SIZECTL, sc,
                                        (rs << RESIZE_STAMP_SHIFT) + 2))
                    // 扩容和数据迁移
                    transfer(tab, null);
            }
        }
    }

大致原理:

  1. 如果table数组没有初始化,则进行初始化;
  2. 如果已经被扩容了,或者数组已经达到最大容量了,直接退出:
  3. 调用transfer(tab, null)函数进行扩容和数据迁移。

接着,查看transfer源码,这个函数可以多个线程并发调用:

    // tab代表旧的table,nextTab代表扩容后的新table
    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        
        // stride可理解成“步长”,即数据迁移时,每个线程要负责旧table中的多少个桶
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        
        // nextTab == null,说明是首次进行扩容
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                // 创建新数组,大小为旧数组的2倍
                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-stride, transferIndex-1]表示当前线程要进行数据迁移的桶区间
            transferIndex = n;
        }
        int nextn = nextTab.length;
        
        // ForwardingNode结点,当旧table的某个桶中的所有结点都迁移完后,用该结点占据这个桶
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        
        // 标识一个桶的迁移工作是否完成,advance == true 表示可以进行下一个位置的迁移
        boolean advance = true;
        
        // 最后一个数据迁移的线程将该值置为true,并进行本轮扩容的收尾工作
        boolean finishing = false; // to ensure sweep before committing nextTab
        
        // i标识桶索引, bound标识边界
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            
            // 每一次自旋前的预处理,主要是定位本轮处理的桶区间
            // 正常情况下,预处理完成后:i == transferIndex-1,bound == transferIndex-stride
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSetInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            
            
            // CASE 1: 当前是处理最后一个tranfer任务的线程或出现扩容冲突
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                // 如果所有桶迁移均已完成
                if (finishing) {
                    nextTable = null;
                    // table指向新的数组
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                
                // 扩容线程数减1, 表示当前线程已完成自己的transfer任务
                if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    // 判断当前线程是否是本轮扩容中的最后一个线程,如果不是,则直接退出
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            
            // CASE2:旧桶本身为null,不用迁移,直接尝试放一个ForwardingNode
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            
            // CASE3:该旧桶已经迁移完成,直接跳过
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            
            // CASE4:该旧桶未迁移完成,进行数据迁移
            else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        
                        // CASE4.1:fh是table[i]的hash值,桶的hash > 0,说明是链表迁移
                        if (fh >= 0) {
                            /**
                             * 下面的过程会将旧桶中的链表分成两部分:ln链和hn链
                             * ln链会插入到新table的槽i中,hn链会插入到新table的槽i+n中
                             */
                            // 由于n是2的幂次,所以runBit要么是0,要么高位是1
                            int runBit = fh & n;
                            // lastRun指向最后一个相邻runBit不同的结点
                            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;
                            }
                            // 以lastRun所指向的结点为分界,将链表拆成2个子链表ln、hn
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            // ln链表存入新桶的索引i位置
                            setTabAt(nextTab, i, ln);
                            // hn链表存入新桶的索引i+n位置
                            setTabAt(nextTab, i + n, hn);
                            // 设置ForwardingNode占位
                            setTabAt(tab, i, fwd);
                            // 表示当前旧桶的结点已迁移完毕
                            advance = true;
                        }
                        
                        // CASE4.2:红黑树迁移
                        else if (f instanceof TreeBin) {
                            /**
                             * 下面的过程会先以链表方式遍历,复制所有结点,然后根据高低位组装成两个链表;
                             * 然后看下是否需要进行红黑树转换,最后放到新table对应的桶中
                             */
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            // 根据阈值,判断是否需要进行 红黑树 <-> 链表 的相互转换
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            // 设置ForwardingNode占位
                            setTabAt(tab, i, fwd);
                            // 表示当前旧桶的结点已迁移完毕
                            advance = true;
                        }
                    }
                }
            }
        }
    }

下面重点强调扩容中的集合要点。

4.2.1 步长的计算

stride是要计算的步长,即数据迁移时,每个线程要负责旧table中的多少个桶:

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

4.2.2 首次扩容需要初始化新的table

如果是首次扩容,先初始化新的table

    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            // 旧table的大小是n,新table大小初始化为2n
            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;
    }

4.2.3 每个线程处理的范围

注意上面的 transferIndex,大小等于n

table[transferIndex - stride, transferIndex - 1]就是当前线程要进行数据迁移的桶区间。 整个 transfer方法几乎都在一个自旋操作中完成,从右往左 开始进行数据迁移,transfer的退出点是当某个线程处理完最后的table区段—— table[0, stride - 1]

4.2.4 链表数据的迁移

CASE 4中,需要分情况处理。

如果table[i]hash大于0,说明table[i]下面连接的是一个链表,进行链表数据的迁移。

链表迁移的过程如下:

  1. 首先会遍历一遍原链表,找到最后一个相邻 runBit不同的结点。 runbit是根据 key.hash和旧table长度 n进行与运算得到的值,由于table的长度为2的幂次,所以 runbit只可能为0或最高位为1

  2. 然后,会进行第二次链表遍历。按照第一次遍历找到的结点为界,将原链表分成2个子链表lnhn

  3. 最后将lnhn这两个链表连接到新的table桶中,ln迁移到新的table[i]位置,hn迁移到新的table[i+n]位置。

4.2.5 红黑树的迁移

如果table[i]的节点类型是TreeBin,说明table[i]下面连接的是一个红黑树。

红黑树的迁移过程如下:

  1. 首先会按照链表遍历的方式e = e.next)去遍历所有节点。在遍历的过程中,会根据每个节点的hash值进行计算(hash & n),根据这个计算结果是不是等于0,将红黑树的节点分成两个链表lohi

  2. 然后会根据是否到达阈值,来决定lohi应该是以链表的形式还是红黑树的形式加入到新的table中;

  3. 最后分别插入到新tabletable[i]table[i+n]下面。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值