Jdk源码之:ConcurrentHashMap (1)

114 阅读4分钟

基于JDK 1.8的源码进行分析;本篇主要分析ConcurrentHashMap的字段和构造器

1. sizeCtl

sizeCtlConcurrentHashMap的一个重要的成员变量;与sizeCtl相关的字段和方法有:

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
    
    /**
     * 这个值应该是final的;变量名不重要,记住它代表常量16即可
     */
    private static int RESIZE_STAMP_BITS = 16;
    
    /**
     * 变量名不重要,记住它代表常量16即可
     */
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
    
    /**
     * sizeCtl字段用于控制哈希表的初始化和扩容,其含义如下:
     * 1. 当sizeCtl等于0时,说明底层数组未初始化,且初始长度为默认长度
     * 2. 当sizeCtl等于-1时,说明底层数组正在初始化
     * 3. 当sizeCtl大于0时,如果底层数组未初始化,则sizeCtl代表其初始长度;如果已经初始化,则代表扩容阈值
     * 4. 当sizeCtl小于-1时,说明底层数组正在扩容,且参与扩容的线程数量是sizeCtl的后16位表示的数字减一
     */
    private transient volatile int sizeCtl;
    
    /**
     * resizeStamp(n)方法在数组扩容时会用到,负责将数组的原长度n转成另一种形式,但本质上还是代表数组的原长度
     * 
     * 当底层数组扩容时,sizeCtl的计算公式为:(resizeStamp(n) << RESIZE_STAMP_SHIFT) + 扩容线程数 + 1
     * 注意RESIZE_STAMP_SHIFT代表常量16,因此sizeCtl的前16位为resizeStamp(n),后16位为扩容线程数 + 1
     * 一定要牢记上面这个结论,并且熟悉resizeStamp(n)方法,不然的话,后面在阅读源码时可能会遇到问题
     */
    static final int resizeStamp(int n) {
        
        // 由于n是2的幂(ConcurrentHashMap的规定),因此可以通过n的二进制数字的前导0的个数来表示n这个数字
        // 另外,这里还或上了(1 << 15),这样的话,本方法的返回值在左移16位后最高位就是1,此时sizeCtl就小于-1了
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }
}

2. 节点类型与哈希值

ConcurrentHashMap中,总共有5种节点类型:

  1. Node:链表节点;是最基本的类型,也是其他类的父类
  2. TreeNode:红黑树节点;代码中很少直接与TreeNode打交道,这是因为TreeNode被封装在TreeBin里面
  3. TreeBin:对红黑树的封装,内部实现了简单的读写锁机制;具体的实现不用去关心,只要记住TreeBin代表红黑树即可
  4. ForwardingNode:重定向节点,其nextTable字段代表新的数组;具体的用途见下面的描述
  5. ReservationNode:占位节点,只在computeIfAbsent()compute()方法中有使用;这个暂时不用去关心

关于ForwardingNode
在进行扩容时,如果旧数组某个桶中的所有数据都已经迁移完成,则该桶中的数据会替换为ForwardingNode
其它线程在操作旧数组时,如果发现桶中是ForwardingNode,就会知道旧数组正在扩容
此时该线程有两种选择:可以帮忙扩容,也可以根据ForwardingNodenextTable字段找到新的数组,然后操作新数组

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
    
    /**
     * 对key的hashCode进行散列操作
     * 和HashMap一样,这里将hashCode的高16位与低16位进行了异或操作
     * 注意,异或操作后还与上了HASH_BITS(0x7fffffff),因此最终的hash是正数
     * 也就是说,普通节点(Node和TreeNode)的hash值都是正数
     */
    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
    
    /**
     * 负数的hash值;用于标识特殊节点
     */
    static final int MOVED     = -1; // hash for forwarding nodes (ForwardingNode)
    static final int TREEBIN   = -2; // hash for roots of trees (TreeBin)
    static final int RESERVED  = -3; // hash for transient reservations (ReservationNode)

    // 由于TreeNode是封装在TreeBin里面的,因此可以得出如下结论:
    // 1. 如果hash为非负数,说明节点类型是Node
    // 2. 如果hash为-1,说明节点类型是ForwardingNode
    // 3. 如果hash为-2,说明节点类型是TreeBin
    // 4. 如果hash为-3,说明节点类型是ReservationNode
}

3. 底层数组相关

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {

    /**
     * ConcurrentHashMap支持多线程并发扩容
     * 而扩容时,线程需要将旧数组的桶中的数据转移到新数组对应的桶中
     * 该值就代表每个扩容线程每次至少应该负责多少个桶的转移
     */
    private static final int MIN_TRANSFER_STRIDE = 16;

    /** 
     * CPU的个数
     */
    static final int NCPU = Runtime.getRuntime().availableProcessors();
    
    /**
     * 最大扩容线程数;虽然值为(1 << 16) - 1,但实际上最多只能有(1 << 16) - 2个线程同时进行扩容
     * 这是因为在源码中,当sizeCtl == (resizeStamp(n) << 16) + MAX_RESIZERS时,就认为扩容线程数达到了最大
     * 由于sizeCtl的后16位代表的是当前扩容线程数 + 1,所以此时的扩容线程其实只有MAX_RESIZERS - 1个
     */
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
    
    /**
     * 底层的链表数组,每个元素相当于一个桶;懒初始化;注意,该数组的长度必须是2的幂
     */
    transient volatile Node<K,V>[] table;
    
    /**
     * 用于存放正在扩容中的链表数组
     */
    private transient volatile Node<K,V>[] nextTable;
    
    /**
     * 该成员变量表示在扩容时,旧数组的哪些桶已经被分配出去了
     * 假设旧数组的长度为64,且每个线程负责16个桶的转移,那么:
     * 1. 在扩容开始前,transferIndex初始化为64
     * 2. 第一个进行扩容的线程会将63号到48号桶分配给自己,然后将transferIndex更新为48
     * 3. 第二个参加扩容的线程会将47号到32号桶分配给自己,然后将transferIndex更新为32
     * 4. 依此类推,当transferIndex小于等于0时,代表所有桶都已经分配出去了
     */
    private transient volatile int transferIndex;
}

4. 构造方法

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
    
    /**
     * 空参构造器,此时sizeCtl初始化为0,代表底层数组的初始长度取默认长度
     */
    public ConcurrentHashMap() {
    }

    /**
     * 指定初始化大小,会计算出第一个比1.5倍initialCapacity大的2的幂作为初始长度赋给sizeCtl
     */
    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;
    }

    /**
     * 根据另一个Map来初始化;此时的初始长度为默认长度
     */
    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }

    /**
     * 指定初始化大小和负载因子;该方法主要是为了兼容JDK 1.7
     * 由于JDK 1.8中ConcurrentHashMap的负载因子固定是0.75,因此这里的loadFactor仅用于计算底层数组的初始长度
     */
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

    /**
     * 指定初始化大小、负载因子和并发级别;该方法主要是为了兼容JDK 1.7
     * 由于JDK 1.8中ConcurrentHashMap的负载因子固定是0.75,因此这里的loadFactor仅用于计算底层数组的初始长度
     * 由于JDK 1.8中ConcurrentHashMap的并发级别固定是底层数组的长度,因此这里的concurrencyLevel仅用于计算底层数组的初始长度
     */
    public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        
        // initialCapacity指定为Math.max(initialCapacity, concurrencyLevel),这也是concurrencyLevel的唯一作用
        if (initialCapacity < concurrencyLevel)
            initialCapacity = concurrencyLevel;
        
        // 计算出能容纳initialCapacity个元素的最小数组长度,这也是loadFactor的唯一作用
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        
        // 计算出第一个大于等于size的2的幂作为初始长度赋给sizeCtl
        int cap = (size >= (long) MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int) size);
        this.sizeCtl = cap;
    }
}