拿捏了!ConcurrentHashMap!

  • Post author:
  • Post category:其他




推荐阅读:



概述

本文将对JDK8中 ConcurrentHashMap 源码进行一定程度的解读。解读主要分为六个部分:主要属性与相关内部类介绍、构造函数、put过程、扩容过程、size过程、get过程、与JDK7实现的简单对比。希望对读者学习ConcurrentHashMap有一定的帮助。

阅读本文前,可能需要读者对HashMap和红黑树等有基本的了解。



主要属性和主要的内部类



主要属性



常量

ConcurrentHashMap中常量一共分为以下几个部分:

  • 容量相关:MAXIMUM_CAPACITY、DEFAULT_CAPACITY、MAX_ARRAY_SIZE
  • 兼容JDK 7而保留的部分常量:DEFAULT_CONCURRENCY_LEVEL、LOAD_FACTOR
  • 红黑树升级和退化相关的常量:TREEIFY_THRESHOLD、UNTREEIFY_THRESHOLD、MIN_TREEIFY_CAPACITY
  • 扩容相关:MIN_TRANSFER_STRIDE、RESIZE_STAMP_BITS、MAX_RESIZERS、RESIZE_STAMP_SHIFT
  • 节点状态常量:MOVED、TREEBIN、RESERVED
    /* ---------------- Constants -------------- */

    /**
     * HashMap的最大容量
     */
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认容量
     */
    private static final int DEFAULT_CAPACITY = 16;

    /**
     * 数组最大长度
     */
    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;

    /**
     * 链表升级成红黑树的阈值
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 红黑树退化成链表的阈值
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 链表升级成树需要满足的最小容量,若不满足,则会先扩容
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    //最小转移步长
    private static final int MIN_TRANSFER_STRIDE = 16;

    //这个常量是用来计算HashMap不同容量有不同的resizeStamp用的
    private static int RESIZE_STAMP_BITS = 16;

    //最大参与扩容的线程数 相当大的一个数 基本上是不会触及该上线的
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

    //要对resizeStamp进行位移运算的一个敞亮
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

    //特殊的节点哈希值
    static final int MOVED     = -1; // hash for forwarding nodes
    static final int TREEBIN   = -2; // hash for roots of trees
    static final int RESERVED  = -3; // hash for transient reservations
    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

    //获取CPU的数量
    static final int NCPU = Runtime.getRuntime().availableProcessors();

其中,因为JDK8中ConcurrentHashMap的实现方式和JDK7的不同,因此DEFAULT_CONCURRENCY_LEVEL已经没有实际作用了。并且在JDK8中,LOAD_FACTOR也已经固定成了0.75f。

另外,MOVED,TREEBIN,RESERVED是用来表示特殊节点的哈希值。该类特殊节点均不含实际元素,且其哈希值被设置为负数和普通节点区分。

剩下的涉及扩容的常量我们在相关的章节中再介绍。



成员变量

通常成员变量都是会负责记录当前类的状态的,ConcurrentHashMap也是如此。因此了解清除成员变量的作用,对我们后续分析ConcurrentHashMap的操作流程很有感帮助。

    /* ---------------- Fields -------------- */

    /**
     * 底层数组
     */
    transient volatile Node<K,V>[] table;
    //扩容时 使用的另一个数组
    private transient volatile Node<K,V>[] nextTable;

    //统计size的一部分
    private transient volatile long baseCount;

    /**
     * sizeCtl与table的resize和init有关
     * sizeCtl = -1时,表示table正在init
     * sizeCtl < 0 且不等于-1时,表示正在resize
     * sizeCtl > 0 时,表示下次需要resize的阈值,即capacity * loadfactory
     */
    private transient volatile int sizeCtl;

    //记录下一次要transfer对应的Index
    private transient volatile int transferIndex;

    //表示是否有线程正在修改CounterCells
    private transient volatile int cellsBusy;

    //用来统计size
    private transient volatile CounterCell[] counterCells;

    // views
    private transient KeySetView<K,V> keySet;
    private transient ValuesView<K,V> values;
    private transient EntrySetView<K,V> entrySet;

同样的,成员变量也可以按作用分成几类:

  • 用作底层数据结构的实现:Node<K,V>[] table
  • 用作统计元素的大小:baseCount、CounterCell[] counterCells、cellsBusy
  • 用作记录ConcurrentHashMap的状态:sizeCtl
  • 用作扩容记录:Node<K,V>[] nextTable、transferIndex
  • 用作转成其它类型的视图:KeySetView<K,V> keySet、ValuesView<K,V> values、EntrySetView<K,V> entrySet

table数组很好理解。因为HashMap的实现是基于数组的,在冲突时通过链地址解决。因此所有的数据都以数组为入口。

另一个数组nextTable是在扩容时备用的。 如果了解Redis的数据结构的读者,应该对这个不陌生,redis渐进式rehash就是通过两个哈希表加一个index实现的。而JDK8中在resize时,也采取了类似的方式(下文我们会介绍到:按步长逐步transfer)。

另外,比较重要的一个属性就是sizeCtl。

如果看过Doug Lea老爷子在JUC下的其他类,经常会有一个特殊的变量表示当前对象的状态。并且已CAS的方式去修改这个变量,实现自旋锁的功能(例如:AQS中的state)。

这里的sizeCtl就是一个富有特殊意义的变量。

当sizeCtl大于0时,表示扩容的阈值(没错,就是HashMap中threshold变量的作用),而且上文我们也了解到在JDK8中由于loadfactor已经被固定为0.75f。因此在正常状态下(非扩容状态),sizeCtl = oldCap >> 1 – (oldCap << 2)。 而sizeCtl == -1是一个特殊的状态标志,表示ConcurrentHashMap正在初始化底层数组。

当sizeCtl为其他负数时,表示ConcurrentHashMap正在进程扩容,其中,高16位可以反应出扩容前数组的大小,而后16位可以反应出此时参与扩容的线程数。



内部类

ConcurrentHashMap拥有大量的内部类,但其中大部分都是用来遍历或是在Fork/Join框架中平行遍历时使用的。这部分类内部类我们不在过多介绍。主要看CountCell和几个Node的类。



CounterCell

首先,CounterCell是用来统计ConcurrentHashMap用的,其内部有个value,用来表示元素个数。size()函数就是通过累加countCells数组中所有CounterCell的value值,再加上BaseCount得到的。相当于ConcurrentHashMap把size这个属性拆散保存在了个多个地方。



Node

同HashMap一样,为了提高链表的遍历速度,ConcurrentHashMap也引用了红黑树。而Node就表示链表中的节点,并且他还是其他节点的父类。



TreeNode

TreeNode表示红黑树中的节点,按照红黑树的标准,它还拥有父节点和左右子节点的属性,此外还需要标识是否为红节点。



TreeBin

TreeBin是一个特殊的节点,用来指向红黑树的根节点,并不存储真实的元素,因此它的节点的哈希值是一个固定的特殊值-2。



ForwardingNode

ForwardingNode和TreeBin一样,并不存储实际元素,而是指向nextTable,哈希值也是一个特殊的固定值(-1)。它在扩容中会使用,表示这个桶上的元素已经迁移到新的数组中去了。



ReservationNode

同样是一个特殊值,在putIfAbsent时使用。因为put时需要对桶上的元素上对象锁(ConcurrentHashMap并非是完全无锁的,只是尽可能少的去使用锁),这时就会添加一个临时占位用的节点ReservationNode。



构造函数

因为构造函数是公有的API,所以必须要和JDK7中保持一致。虽然其中的部分含义可能发生了一些变化。

我们看一下参数最全的构造函数。

    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) ?
 



版权声明:本文为lonely_bin原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。