ConcurrentHashMap底层源码分析

文章目录

  • ConcurrentHashMap底层源码分析
    • 1. 成员变量
    • 2. 构造方法
      • 2.1 不指定初始容量
      • 2.2 指定初始容量
    • 3. put方法添加节点
      • 3.1 spread()方法
      • 3.2 initTable()方法
      • 3.3 tabAt( )方法
      • 3.4 casTabAt( ) 方法
    • 4. get 方法

ConcurrentHashMap底层源码分析

1. 成员变量

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V> {
    /**
     * 数组的最大容量
     */
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 数组初始化的默认容量,必须是2的n次方
     */
    private static final int DEFAULT_CAPACITY = 16;

    /**
     * 数组的最大size
     */
    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;

    /**
     * 当桶(bucket)上的结点数大于8时会转成红黑树
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 当桶(bucket)上的结点数小于6时树转链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 桶中结构转化为红黑树对应的数组长度最小的值 
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * 每次进行转移的最小值
     */
    private static final int MIN_TRANSFER_STRIDE = 16;

    /**
     * 生成sizeCtl所使用的bit位数
     */
    private static int RESIZE_STAMP_BITS = 16;

    /**
     * 进行扩容时的最大线程数
     */
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

    /**
     * 记录sizeCtl中的大小所需要进行的偏移位数
     */
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
    
    
	// 0:默认为 0,代表数组还未初始化,且数组的初始化容量为16
    // 负数:当数组正在初始化时, 为 -1 
    // 负数:当数组正在扩容时, 为 -(1 + 扩容线程数)
    // 正数:当初始化还未初始化时代表数组的初始容量,当数组初始化后为数组扩容的阈值大小:数组的初始容量*0.75
    private transient volatile int sizeCtl;
    
    static final int MOVED     = -1; // 表示正在转移
    static final int TREEBIN   = -2; // 表示已经转换成树
    static final int RESERVED  = -3; // hash for transient reservations
    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
    transient volatile Node<K,V>[] table;//默认没初始化的数组,用来保存元素
    private transient volatile Node<K,V>[] nextTable;//转移的时候用的数组
}

2. 构造方法

2.1 不指定初始容量

实际使用时,我们一般不使用空参数构造函数,调用带参构造方法,指定一个初始容量,因为如果不指定初始容量的话,他会在第一次调用put()方法时,对数组容量进行扩容并初始化,扩容比较影响性能。

/**
 * Creates a new, empty map with the default initial table size (16).
 * 不指定数组容量,扩容时扩容到默认数组容量为16
 */
public ConcurrentHashMap() {
}

2.2 指定初始容量

如果指定的初始容量不是2的n次方,会调用 tableSizeFor(int c)方法,将数组的容量增加至2的n次方:与HashMap不同的是,如果我们传入的容量为16,那么返回的容量也为16,但是ConcurrentHashMap1.8中,如果传入的容量为16,返回的容量为32

/**
 * Creates a new, empty map with an initial table size
 */
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0) throw new IllegalArgumentException();
    //将输入的数组容量增加至2的n次方
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               //在HashMap中是tableSizeFor(initialCapacity),如果传入16,返回仍然是16
               //注意:在这里,如果传入16,返回的是32
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    //如果在实例化对象的时候指定了容量,则初始化sizeCtl
    this.sizeCtl = cap;
}
/**
 * Returns a power of two table size for the given desired capacity.
 */
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;
}

3. put方法添加节点

ConcurrentHashMap能够保证线程安全的向集合中添加元素:

① 对数组进行初始化时,使用CAS+自旋的方式,初始化数组

② 如果要添加元素的桶位处没有元素,那么使用CAS+自旋的方式向桶位中添加元素

③ 如果要添加元素的桶位中有元素,那么使用Synchronized给当前桶位加锁,锁对象为桶位中的头节点对象,这样 可以线程安全的向桶中添加元素。

put方法流程总结:

① 添加数据时,首先判断当前数组是否为null,如果为null ,使用CAS+自旋的方式,初始化数组。

② 如果数组不为null,判断桶中元素的key的hash值是否为-1,如果为-1,说明当前数组正在扩容,即将旧数组中的元素复制到新数组中,当前线程帮助数组扩容。

③ 如果数组不为null,并且数组也不再扩容,根据hash值计算要输入数据在数组中的桶下标,取出桶中的元素,并判断是否为null,如果为null,那么采用CAS+自旋的方式线程安全的将当前节点添加到桶中。

④ 如果桶中的元素不为null,那么使用Synchronized对当前桶中加锁,即桶对象为当前桶的头节点对象,如果其他线程想要向这个桶中加锁,需要先获的这个锁对象,可以保证向这个桶中添加节点元素时线程安全的。

⑤ 判断桶中元素的节点Key的hash值是否小于0,如果小于说明是链表,那么遍历链表,判断链表中是否存在于要添加元素的key重复的,如果有键相同值覆盖,如果没有直接添加到链表的尾部;如果不是链表节点判断是否是红黑树节点,如果是的话,调用红黑树添加节点的方法。

⑥ 添加完节点后,判断在该桶中添加的节点数是否大于8,如果大于8,将链表转换为红黑树。

⑦ 调用addCount()方法计算集合的长度,并判断是否需要扩容。

public V put(K key, V value) {
    return putVal(key, value, false);
}
/**
① 当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,如果没有的话就初始化数组
② 计算hash值来确定要添加的数据放在数组的哪个位置,即数组的桶下标,并通过CAS原子操作取出该位置元素
③ 如果该位置为空则直接添加,如果不为空的话,则取出这个节点元素
④ 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,
	  将原数组复制到新的数组,则当前线程也去帮助复制
⑤ 最后一种情况就是,如果这个节点不为空,也不在扩容,则通过synchronized来加锁,进行添加节点操作
     判断当前取出的节点位置存放的是链表还是树:
     如果是链表的话,则遍历整个链表,将取出来的节点的key来个要放的key进行比较,
     如果key相等,且key的hash值也相等,则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾
     如果是树的话,则调用putTreeVal方法把这个元素添加到树中去
⑥ 最后在添加完成之后,会判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话,
     则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
    //注意:HashMap中允许空键空值,但是ConcurrentHashMap不允许
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());  //基于key计算hash值
    int binCount = 0;  //记录某个桶上元素的个数,如果超过了8个,就会从链表转换为红黑树
    for (Node<K,V>[] tab = table;;) {   //死循环,除非break,否则一直执行
        Node<K,V> f; int n, i, fh;
        //① 如果数组还未初始化,先初始化数组
        if (tab == null || (n = tab.length) == 0)  
            tab = initTable();
        //② 根据hash值计算数组桶下标,并通过CAS原子操作取出该位置的元素,判断是否为null
        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;                   
        }
        //③ 如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩容的数据复制阶段
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        //④ 当前线程不为null,并且也不在扩容,那么使用Synchronize加锁向桶中该位置添加节点
        else {
            V oldVal = null;
            //加锁,锁对象是数组桶中的头结点
            synchronized (f) {
                if (tabAt(tab, i) == f) { // 再次取出要存储的位置的元素,跟前面取出来的比较
                    if (fh >= 0) { // 取出来的元素的hash值大于0,说明为链表;当转换为树之后,hash值为-2
                        binCount = 1;
                        //遍历链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 要存的元素的hash,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;
                            // 遍历到链表尾部都没有重复的key,那么将节点添加到链表的尾部
                            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;
                        }
                    }
                }
            }
            // 在添加完之后,会对该节点上关联的的数目进行判断,
            // 如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容(数组长度小于64)
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //计算集合长度,判断是否需要扩容
    addCount(1L, binCount);
    return null;
}

① 当数组正在扩容时,就会将原数组中的节点从后向前搬迁到新的数组中,搬迁完以后会在该桶中的头节点处添加一个ForwordingNode节点,凡是带有这个节点的代表数组正在扩容,同时当前节点的hash设置为MOVED,此时不能向这个数组中添加元素,当前线程只能协助扩容

ConcurrentHashMap底层源码分析_第1张图片

② 关于锁对象的说明:我们将数组对应桶处的首节点对象作为锁对象,这样当其他线程想要向该桶中添加元素时,需要先获取锁对象,可以保证在向这个桶中添加元素时是线程安全的。比如对于桶下标为1的桶的首节点为柳岩,那么如果当前线程已经获取了锁对象柳岩节点,并且正在向这个桶中添加元素,其他线程是无法向该桶中添加元素的。

注意:向桶下标为1的桶中添加元素,是不会影响其他线程向其他桶下标为2的桶中添加元素的。这是与HashTable不同的地方,因为HashTable在向数组中添加元素时,整个数组都会锁住,但是ConcurrentHashMap只会锁住要添加元素位置处的那个桶。
ConcurrentHashMap底层源码分析_第2张图片

3.1 spread()方法

static final int spread(int h) {
    //通过高16bit 和 低16bit 混合计算出 16bit 的哈希值,充分利用所有信息计算出哈希值,减少hash冲突  
    return (h ^ (h >>> 16)) & HASH_BITS;
}

3.2 initTable()方法

/**
 	 * Initializes table, using the size recorded in sizeCtl.
     * 初始化数组table,
     * 如果sizeCtl小于0,说明别的数组正在进行初始化,则让出执行权
     * 如果sizeCtl大于0的话,则初始化一个大小为sizeCtl的数组
     * 否则的话初始化一个默认大小(16)的数组
     * 然后设置sizeCtl的值为数组长度的3/4,作为下次扩容时的阈值
 */
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    // 利用CAS+自旋的方式将SIZECTL设置为-1
    while ((tab = table) == null || tab.length == 0) {
        // 如果sizeCtl小于0,说明别的数组正在进行初始化,则让出CPU执行权
        if ((sc = sizeCtl) < 0)
            Thread.yield(); 
        // 如果sizeCtl大于0,则初始化一个大小为sizeCtl的数组
        // 通过CAS+自旋线程安全的将SIZECTL设置为-1,sc表示期望值,-1表示要替换的值,-1表示要初始化table了
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                // double-check方式,双重校验
                if ((tab = table) == null || tab.length == 0) {
                    // sc>0说明已经指定了初始化容量,数组容量初始化为指定大小,否则初始化为默认大小16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    // 根据数组容量大小n创建一个数组,完成数组的初始化
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    //n-0.25*n = 0.75*n =>初始化完成后,sizeCtl变为下次扩容的阈值
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    //返回初始化后的数组
    return tab;
}

3.3 tabAt( )方法

//CAS原子操作:返回数组指定位置处的节点
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

3.4 casTabAt( ) 方法

// cas原子操作,在指定位置设定值
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);
}

4. get 方法

https://www.cnblogs.com/zerotomax/p/8687425.html#go6

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        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;
}

你可能感兴趣的:(Java并发编程)