ConcurrentHashMap源码讲解


前言

由于 HashMap 是线程不安全的,无法运用到高并发的环境中,这时候线程安全的 ConcurrentHashMap 就有用武之地了。
ConcurrentHashMap 是基于分段锁(synchronized)和 CAS 来保证线程安全的。

一、重要常量和变量

// 数组最大长度 = 2^30
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 数组默认长度 = 16
private static final int DEFAULT_CAPACITY = 16;
// 加载因子=0.75,当数组使用率达到0.75时,会进行扩容
private static final float LOAD_FACTOR = 0.75f;
// 树化条件1:链表长度>8
static final int TREEIFY_THRESHOLD = 8;
// 取消树化条件:链表长度<6
static final int UNTREEIFY_THRESHOLD = 6;
// 树化条件2:数组长度>=64
static final int MIN_TREEIFY_CAPACITY = 64;
// 扩容转移最少需要16个索引,因为扩容时其他线程可以帮忙,所以限定每个线程转移槽位数最小为16
private static final int MIN_TRANSFER_STRIDE = 16;
// 当数组索引位的节点的hash等于这个常量时,表示正在扩容
static final int MOVED     = -1; 
// 此元素后为红黑树
static final int TREEBIN   = -2; 
// 表示临时保留的hash
static final int RESERVED  = -3; 
// 是一个16进制的数,大小位2^31-1,作用是将key的hash转成正数
static final int HASH_BITS = 0x7fffffff;
// 数组
transient volatile Node<K,V>[] table;
// 数据转移时的中间表
private transient volatile Node<K,V>[] nextTable;
// 是一个非常重要的变量。用来表示数组初始化和扩容的,
// 当为0时,表示还未初始化;
// 当为-1时,表示正在初始化;
// 小于-1时,表示正在扩容;
// 大于0时,表示初始化完成,这时候等于0.75*数组长度
private transient volatile int sizeCtl;
//数据转移时,当前已经迁移的元素下标
private transient volatile int transferIndex;


二、构造器(5个)

// 构造器一
public ConcurrentHashMap() {
}

// 构造器二
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;
}

// 构造器三
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;
}

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

总共有五个构造器,这里就讲解比较常用的。

构造器二:参数是一个初始容量,先进行非法从参数判断,接着判断参数是否超过最大允许容量的一半( >>> 无符号位移,一半也就是 2^29),超过了就将 sizeCtl (数组未初始化时表示为数组长度)赋值为 2^30 ,否则就将 参数×1.5+1 ,并且调用 tableSizeFor(...) 方法赋值。 tableSizeFor(...) 这个方法的作用是保证数组的长度为 2 的幂次,它最终得到一个 2 的幂次值,并且这个值大于等于且最接近参数。比如构造器参数 initialCapacity=16 ,那么 16*1.5+1=25 ,最接近的是 32 ,所以 sizeCtl=32

注意:为什么要将 初始容量×1.5+1

假设我现在要存 16 个键值对,然后 new ConcurrentHashMap(16) ,如果只创建16 长度的数组在散列均匀的情况下肯定不够的(阈值为 0.75,也就是在大于 12 的时候就开始扩容了),这时候就需要扩容了。所以为了解决这个问题,需要将 16÷0.75 ,也就是 16×1.333 ,为了提高效率,将 16×1.5 采用位运算,而 +1 是为了进 1 ,弥补数据。



put(…)方法

1.putVal(…)方法

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

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 不允许null值null键
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());  // 通过hash算法获得hash值,下面展示
    int binCount = 0;
    // 并发情况下插入可能会失败,而这自旋是保证插入一定成功
    for (Node<K,V>[] tab = table;;) {  
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)  // 如果数组还未初始化,那么就去初始化
            tab = initTable();
        // 如果数组当前索引位置为空
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 使用CAS执行插入操作
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;                  
        }
        // 当数组索引位置的节点为-1时,说明有线程正在扩容
        else if ((fh = f.hash) == MOVED) 
            tab = helpTransfer(tab, f); // 帮助扩容
        else {   // 出现hash冲突
            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;
                            // key已经存在,则value覆盖
                            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;
                            }
                        }
                    }
                    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) {
                if (binCount >= TREEIFY_THRESHOLD) // 当链表长度大于8,也就是9时,调用树化方法
                    treeifyBin(tab, i);
                if (oldVal != null) // 如果key已经存在,就将原来的value值返回
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount); // 扩容方法就在里面
    return null;
}

// 求hash的算法,h ^ (h >>> 16)与前面讲的HashMap相同,& HASH_BITS为了保证得到的hash一定是个非负数
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

总体来说和 HashMap 是非常接近的,就是多了 synchronizedCAS

第一次 put(...) 时,数组还未进行初始化,调用 initTable() 方法进行初始化。初始化完后,进行第二次循环,根据 hash 算出索引位置((n - 1) & hash)),这时索引位肯定是空的,所以直接使用 CAS 将新节点赋在索引位上,

  • 赋值成功就跳出循环,调用 addCount(...) 方法;

  • 赋值失败(其他线程赋值成功)就进行第三次循环,这个时候索引位置已经不为空了,所以开始获取锁,获取锁成功后,开始对索引位置的节点进行遍历,判断是否有 key 相同的:

    • 有相同的 value 就将覆盖。并且将原来的 value 返回;
    • 没有就创建新节点,同时将尾节点的 next 指针指向新节点,调用 addCount(...) 方法;


2. initTable()方法–初始化数组

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0) // 有其他线程正在初始化
            Thread.yield();  
        // CAS将sizeCtl设为-1,保证只有一个线程正在执行扩容
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { 
            try {
                if ((tab = table) == null || tab.length == 0) {
                    // 这里的sc就是sizeCtl (还未经过前面CAS设置的值)
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY; 
                    @SuppressWarnings("unchecked")
                    // 创建数组
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2); //n-0.25n=0.75n
                }
            } finally {
                sizeCtl = sc; //将sizeCtl设置为数组阈值,也就是0.75n
            }
            break;
        }
    }
    return tab;
}

线程进入 initTable() 方法时,先进入 while 循环,这个循环为了让那些失去初始化竞争的线程进行自旋,并且初始化成功后跳出循环。

  • 当成员变量的 sizeCtl 小于 0 时,说明有线程正在初始化,那么就暂停当前线程,执行其他线程

  • 当成员变量的 sizeCtl 不小于 0 ,并且 CAS 设置成功,就创建一个节点数组,根据 sizeCtl 确定数组长度:

    • sizeCtl 大于 0 ,数组长度为 sizeCtl

    • sizeCtl 不大于 0 ,数组查高度为默认值 16

最后将 sizeCtl 设置为数组阈值( 数组长度×0.75



3. 其他四个方法

helpTransfer(...) :帮助扩容,每个线程最少需要转移 16 个槽位

treeifyBin(...) :树化方法,树化条件与 HashMap 一致

addCount(...) :计数,将元素个数 +1 ,并且判断是否符合扩容条件,符合就进行扩容(也可能是帮助其他线程扩容)

transfer(...) :扩容方法

在这里这四个方法就不详细讲解了,主要原因是因为在多线程情况下,有无数种可能,单靠文字描述非常费力,而且 ConcurrentHashMap 非常复杂,了解上面那部分其实也就差不多了,没必要每个方法都要一行一行代码去扣,毕竟人的精力是有限的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值