写点什么

阿里 P7 大佬!王者级讲解 ConcurrentHashMap 源码,码农:太透彻了

发布于: 2021 年 05 月 10 日
阿里P7大佬!王者级讲解ConcurrentHashMap源码,码农:太透彻了

上一篇是分享的是《Scala - 类的定义》,这篇给大家分享《ConcurrentHashMap 源码》。今天先更新一部分,一会儿要出门,谅解。

ConcurrentHashMap 源码解读一

首先就先来说一下几个全局变量

private static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量2的30次方
    private static final int DEFAULT_CAPACITY = 16; //默认容量  1<<4
    
    private static final float LOAD_FACTOR = 0.75f;  //负载因子
    static final int TREEIFY_THRESHOLD = 8;  //链表转为红黑树,大于8小于6先对链表数组进行翻倍扩容操作
    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;//stamp高位标识移动位数
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
    static final int MOVED     = -1; // forwarding nodes 的hash值,如果hash值等于-1代表线程协助扩容
    static final int TREEBIN   = -2; // roots of trees 的hash值,如果hash等于-2代表,当前桶是红黑树
    static final int RESERVED  = -3; // transient reservations 的hash值

    // usable bits of normal node hash,在hash计算的时候运用到,与HashMap计算出来的hash值进行与操作
    static final int HASH_BITS = 0x7fffffff; 
    
    static final int NCPU = Runtime.getRuntime().availableProcessors(); //可用处理器数量

然后是几个全局属性

transient volatile Node<K,V>[] table;//当前ConcurrentHashmap的Node数组,正在使用的数组

private transient volatile Node<K,V>[] nextTable;//ForwardNode所指向的下一个表,正在扩容的数组(还未使用)


private transient volatile long baseCount;//如果使用CAS计数成功,使用该值进行累加,计数用的

//扩容设置的参数,默认为0,当值=-1的时候,代表当前有线程正在进行扩容操作
//当值等于-n的时候,代表有n个线程一起扩容,其中n-1线程是协助扩容
//当在初始化的时候指定了大小,这会将这个大小保存在sizeCtl中,大小为数组的0.75
private transient volatile int sizeCtl;//标记状态以及数组阈值


private transient volatile int transferIndex;//数组扩容的时候用到


private transient volatile int cellsBusy;

//如果使用CAS计算失败,也就是说当前处于高并发的情况下,那么
//就会使用CounterCell[]数组进行计数,类似jdk1.7分段锁的形式,锁住一个segment
//最后size()方法统计出来的大小是baseCount和counterCells数组的总和
private transient volatile CounterCell[] counterCells;//计数数组。

首先是有参构造,这里如果是

ConcurrentHashMap chm = new ConcurrentHashMap(15);

那么其实容量不是 15,而是 32;

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

从这里可以看出是看 tableSizeFor 这个方法的,15+7+1=23;

private static final int tableSizeFor(int c) {
        int n = c - 1;//23-1=22  0b10110
        n |= n >>> 1;// 10110 | 01011 = 11111,下面都是11111也就是31
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//31+1 =  32
    }

所以这就是最后的容量,为 32,也就是有参的参数的两倍最近的 2 的次方数。

接下来就将 put 方法

final V putVal(K key, V value, boolean onlyIfAbsent) {//onlyIfAbsent跟hashmap一样,就是判断是否要覆盖,默认为false,覆盖。
        if (key == null || value == null) throw new NullPointerException();////这句话可以看出,ConcurrentHashMap中不允许存在空值,这个是跟HashMap的区别之一 //通过这个机制,我们可以通过get方法获取一个key,如果抛出异常,说明这个key不存在
        int hash = spread(key.hashCode());//这个方法就相当于基于计算hash值。
        int binCount = 0;//这个是记录这个桶的元素个数,目的是用它来判断是否需要转换红黑树,
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
        //情况一:如果数组为空或者长度为0,进行初始化工作
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
        //情况二:如果获取的位置的节点为空,说明是首节点插入情况,也就是该桶位置没有元素,利用cas将元素添加。
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,//cas加自旋(和外侧的for构成自旋循环),保证元素添加安全
                             new Node<K,V>(hash, key, value, null)))//如果加成功了,那么就break,否则再经过for的死循环进行判断
                    break;                   // no lock when adding to empty bin
            }
        //情况三:如果hash计算得到的桶的位置元素的hash值为moved,也就是-1,证明正在扩容,那么就协助扩容。
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
         //hash计算的桶位置元素不为空,且当前没有处于扩容操作,进行元素添加
        //情况四:这个桶有元素,则执行插入操作,有两种可能,一是这个桶对应的链表没有相同的key,那么久再链表尾插入node节点,而是有相同的key,那么久替换其value。
            else {
                V oldVal = null;
                synchronized (f) { //对当前桶进行加锁,保证线程安全,执行元素添加操作 //将桶位置的元素锁住,那么在该桶位中的链表或者红黑树进行添加元素的话,就是安全的,只有这个线程拿住了这个锁
                    if (tabAt(tab, i) == f) {//因为添加元素之后可能链表已经变成红黑树了,那么这个f就可能变化了。所以要再进行判断。
                        if (fh >= 0) {//说明是普通链表节点
                            binCount = 1;
                            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) {//尾插法,如果e的下一个不是null,那么循环会让pred变成e,直到最后节点,此时e的下一个为null的话
                            //那么也就是pred下一个为null,那么插入到pred下一个即可。
                                    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,有可能将链表转成红黑树,因为在treeifyBin(tab, i);方法中还有一个判断数组长度是否小于64的判断,如果小于64,就不会
                //树化。只是数组扩容。
                        treeifyBin(tab, i);
                    if (oldVal != null) //如果是重复键,直接将旧值返回
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);//添加的是新元素,维护集合长度,并判断是否要进行扩容操作
        return null;
    }

总结:并发 map,jdk1.8 的情况下,底层跟 hashmap 一样,也是数组加链表加红黑树。

  • 以上就是《ConcurrentHashMap 源码解读一》的分享。

  • 也欢迎大家交流探讨,该文章若有不正确的地方,希望大家多多包涵。

  • 你们的支持就是我最大的动力,如果对大家有帮忙给个赞哦~~~

用户头像

还未添加个人签名 2021.04.26 加入

还未添加个人简介

评论

发布
暂无评论
阿里P7大佬!王者级讲解ConcurrentHashMap源码,码农:太透彻了