• 源码解析系列:ConcurrentHashMap(1) - 构造方法和其他参数




    前言

    ConcurrentHashMap作为面试的重点,从这篇文章开始就来尝试去解析内部的方法。对比HashMap,最大的区别就是ConcurrentHashMap是并发安全的。文章参考视频:concurrentHashMap源码解析。文章对于树化这一些操作不会详细说明,有兴趣可以去参考之前的HashMap的文章:

    第一篇文章:源码解析系列:HashMap(1)
    第二篇文章:源码解析系列:HashMap(2)
    第三篇文章:源码解析系列:HashMap(3)
    第四篇文章:源码解析系列:HashMap(4)
    第五篇文章:源码解析系列:HashMap(5)


    1. 使用HashMap的问题

    单纯从源码上来看,HashMap的一个很明显的问题就是并发不安全。特别是当两个线程都执行 put 操作的时候容易造成一个线程的 put 结果会被另一个线程所覆盖掉。此外,当一个线程使用 put 方法或者 delete 方法的时候如果造成了 HashMap 的扩容或者树化等操作,改变了原来的内存的地址空间,那么这时候如果另一个线程在读取数据的时候就有可能获取到 null 值。

    所以下面就引入了 concurrentHashMap,concurrentHashMap 和 Map 在1.8的底层存储是一样的



    2. 构造方法

    //无参构造
    public ConcurrentHashMap() {
    }
    
    //初始容量
    public ConcurrentHashMap(int initialCapacity) {
    	//判断初始容量参数有效性
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        //MAXIMUM_CAPACITY >>> 1 = 1073741824 = Integer.MAX_VALUE/2
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   //tableSizeFor: 这个方法就是把一个数变成比这个数大的2的n次方倍
                   //31 => 32, 49 => 64
       			   //这里参数是传入1.5initialCapacity+1进行转化
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        //赋值给sizeCtl
        this.sizeCtl = cap;
    }
    
    //使用Map初始化
    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
     	this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }
    
    //初始化初始容量和加载因子
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
     	this(initialCapacity, loadFactor, 1);
    }
    
    //初始化初始容量和加载因子以及concurrencyLevel
    //concurrencyLevel:并发更新线程的估计数量,用来预测有多少个线程会参加并发
    //根据并发线程数目和传入的参数来初始化最开始的容量大小
    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
        // size = 1 + 初始容量/负载因子
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        //最后也是调用tableSizeFor来计算
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        //赋值给sizeCtl
        this.sizeCtl = cap;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    在上面的构造方法中,public ConcurrentHashMap(int initialCapacity) 这个构造方法中的初始容量和 HashMap 中的不同,这里如果我们在 HashMap 中传入一个 32 的容量,那么 HashMap 中初始化出来的还是 32,但是这里构造方法初始化出来的是 64,因为传入 32 ,但是实际上调用 tableSizeFor 传入的参数是 1.5*32 + 1,也就是传入49,然后一顿操作之后结果是64。

    结论就是:初始容量是比传入容量大的 2 的 n 次方。



    3. sizeCtl

    上面的构造方法中出现了 sizeCtl 这个值,这个值在后面的方法中也起到了很重要的作用,下面是一些值和里面包含的含义:

    • value = 0,代表数值未初始化,且数组的初始容量值为16
    • value = 正数,如果数组没有初始化,那么记录的就是容量值,如果初始化了记录的就是数组的扩容阈值。初始化发生在第一次 put
    • value = -1,表示数组正在初始化
    • value 小于0,并且不是-1,表示数组正在扩容,-(1 + n),表示此时有 n 个线程正在共同完成数组的扩容操作



    4. 其他参数

    1. 常量

    //1. 最大的容量/2
    private static final int MAXIMUM_CAPACITY = 1 << 30;
    
    //2. 默认初始容量是16
    private static final int DEFAULT_CAPACITY = 16;
    
    //3. 最大的数组容量-8,在ArrayList中使用这个判断是否快到了数组的最大容量Integer.MAX_VALUE
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    //4. 默认并发级别。未使用,但为了与此类的以前版本兼容而定义。
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    
    //5. 负载因子
    private static final float LOAD_FACTOR = 0.75f;
    
    //6. 树化节点阈值,链表节点大于8个就树化成红黑树
    //注意树化的时候不仅维护了红黑树还维护了双向链表
    static final int TREEIFY_THRESHOLD = 8;
    
    //7. 去树化节点阈值,树节点 < 6 的时候就去树化为链表
    static final int UNTREEIFY_THRESHOLD = 6;
    
    //8. 表长度 < 64 的时候尝试扩容,该值应至少为4 * TREEIFY_THRESHOLD,以避免调整大小阈值和树化阈值之间的冲突
    //也就是说树化的条件是满足链表节点 > 8 并且表长度 > 64, 也就是数组长度 > 64
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    //9. 数组细分默认范围,用于扩容移动数组
    private static final int MIN_TRANSFER_STRIDE = 16;
    
    //10. size中用于生成戳记的位数。对于32位数组必须至少为6。
    private static int RESIZE_STAMP_BITS = 16;
    
    //11. 帮助扩容的最大线程数
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
    
    //12. 在sizeCtl中记录大小戳的位移位。
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
    
    //13. Node节点的hash值,有一下四个
    static final int MOVED     = -1; //第一个节点,如果是moved,就证明正在进行扩容,那么多线程下其他线程就会帮助扩容
    static final int TREEBIN   = -2; //树节点
    static final int RESERVED  = -3; //用于临时保留的哈希
    static final int HASH_BITS = 0x7fffffff; //普通节点
    
    //CPUS的数目,因为线程数始终得和CPU数挂钩,比如4核CPU,那最多就并发4了
    static final int NCPU = Runtime.getRuntime().availableProcessors();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46



    2. Node链表节点

    还有一个 Node 链表节点:

    //单链表节点
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
    
        Node(int hash, K key, V val, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = 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 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)));
        }
    
       
        //用来给子类重写的,里面表现在TreeNode重写
        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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46



    3. TreeNode树节点

    树节点TreeNode:

    static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  //父结点
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    //前一个节点,去树化用到
        boolean red;
    
        TreeNode(int hash, K key, V val, Node<K,V> next,
                 TreeNode<K,V> parent) {
            super(hash, key, val, next);
            this.parent = parent;
        }
    
        Node<K,V> find(int h, Object k) {
            return findTreeNode(h, k, null);
        }
    
        
        //根据key返回
        final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
            if (k != null) {
                TreeNode<K,V> p = this;
                do  {
                    int ph, dir; K pk; TreeNode<K,V> q;
                    TreeNode<K,V> pl = p.left, pr = p.right;
                    if ((ph = p.hash) > h)
                        p = pl;
                    else if (ph < h)
                        p = pr;
                    else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                        return p;
                    else if (pl == null)
                        p = pr;
                    else if (pr == null)
                        p = pl;
                    else if ((kc != null ||
                              (kc = comparableClassFor(k)) != null) &&
                             (dir = compareComparables(kc, k, pk)) != 0)
                        p = (dir < 0) ? pl : pr;
                    else if ((q = pr.findTreeNode(h, k, kc)) != null)
                        return q;
                    else
                        p = pl;
                } while (p != null);
            }
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48



    4. 变量

    
    //1. 数组,长度为2的n次方倍
    transient volatile Node<K,V>[] table;
    
    //2. 扩容的时候使用,用来记录扩容后的数组
    private transient volatile Node<K,V>[] nextTable;
    
    //3. 基础值,默认为0,baseCount在首次添加元素的时候设置为1
    private transient volatile long baseCount;
    
    //4. 上面有介绍
    private transient volatile int sizeCtl;
    
    //5. 扩容期间用于分割线程处理的范围下标
    private transient volatile int transferIndex;
    
    //6. 自旋锁(通过CAS锁定)在调整大小或创建countercell时使用
    private transient volatile int cellsBusy;
    
    //7. 线程添加数的时候是添加到这里面的,数组是2的n次方倍
    private transient volatile CounterCell[] counterCells;
    
    //普通变量
    private transient KeySetView<K,V> keySet;
    private transient ValuesView<K,V> values;
    private transient EntrySetView<K,V> entrySet;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27





    如有错误,欢迎指出!!!!

  • 相关阅读:
    精品基于NET实现的论坛管理系统
    AI实战 | 手把手带你打造校园生活助手
    第二十届北京消防展即将开启,汉威科技即将精彩亮相
    物联网协议基础知识
    SpringMVC(一)
    JS实现中文转拼音首字母和五笔简码
    【Maven入门篇】(1)详细讲解Maven的安装&&报错解决
    Flink Operator 使用指南 之 Flink Operator安装
    Purple-Pi-OH Linux SDK编译手册
    使用 PyTorch 实现 Word2Vec 中Skip-gram 模型
  • 原文地址:https://blog.csdn.net/laohuangaa/article/details/126157613