【JUC】ConcurrentHashMap源码分析

注释取数方法没有加锁,所以会被存数方法影响聚合方法:size/isEmpty/containsValue,在没有被并发更新的情况下是准确的,但是存在并发更新时,上述聚合方法只是反映了map的一个瞬时状态,这种瞬时状态只能用于监测或估算,而不能用于程序控制和Hashtable一样,和HashMap相反,不允许使用null作为key或value属性常量MAXIMUM_CAPACITYmap在确定数组下标时,采用的是(length-1)&hash的方式,只有当length为2的指数幂的
摘要由CSDN通过智能技术生成

类注释

  • 取数方法没有加锁,所以会被存数方法影响
  • 聚合方法:size/isEmpty/containsValue,在没有被并发更新的情况下是准确的,但是存在并发更新时,上述聚合方法只是反映了map的一个瞬时状态,这种瞬时状态只能用于监测或估算,而不能用于程序控制
  • 和Hashtable一样,和HashMap相反,不允许使用null作为key或value

属性

常量

MAXIMUM_CAPACITY

  • map在确定数组下标时,采用的是(length-1)&hash的方式,只有当length为2的指数幂的时候才能较均匀的分布元素。所以map规定了其容量必须是2的n次方,使用位运算同时还提高了Java的处理速度
  • map内部由Entry[]数组构成,Java的数组下标是由int表示的。所以对于map来说其最大的容量应该是不超过int最大值的一个2的指数幂,而最接近int最大值的2的指数幂用位运算符表示就是1<<30

LOAD_FACTOR

  • 在构造函数中指定loadFactor只能影响初次构造的map的capacity,后续不会用到
  • 其实LOAD_FACTOR的值不会常用,因为直接用n-(n>>>2)即表示n的3/4,即n*LOAD_FACTOR

TREEIFY_THRESHOLD/UNTREEIFY_THRESHOLD

  • TREEIFY_THRESHOLD设置为8的原因:TreeNode占用空间是Node的两倍,而且结点数较少时,红黑树的查找效率跟链表相差不大,所以只在结点数量较多时用红黑树才能得到时间和空间上的tradeoff。在随机hash和LOAD_FACTOR的前提下,bins中的结点分布符合泊松分布,在一个bin中节点数达到8的概率是0.00000006,所以使用8作为阈值是很考究的。
  • UNTREEIFY_THRESHOLD设置为6的原因:使用6也是一个tradeoff,在TREEIFY_THRESHOLD使用8的前提下,如果UNTREEIFY_THRESHOLD使用7,则可能存在对同一个bin频繁插入和删除的操作就会导致bin频繁的转为红黑树和链表,如果UNTREEIFY_THRESHOLD使用6以下的值,那红黑树在空间和时间效率上并不比链表优良,所以使用6是最合适的

MIN_TRANSFER_STRIDE

  • 扩容操作中,transfer这个步骤是允许多线程的,这个常量表示一个线程执行transfer时,最少要对连续的16个hash桶进行transfer(不足16就按16算,多控制下正负号就行)。也就是单线程执行transfer时的最小任务量,单位为一个hash桶,这就是线程的transfer的步进(stride)
  • 最小值是DEFAULT_CAPACITY,不使用太小的值,避免太小的值引起transfer时线程竞争过多,如果计算出来的值小于此值,就使用此值。正常步骤中会根据CPU核心数目来算出实际的,一个核心允许8个线程并发执行扩容操作的transfer步骤,这个8是个经验值,不能调整的
  • 因为transfer操作不是IO操作,也不是死循环那种100%的CPU计算,CPU计算率中等,1核心允许8个线程并发完成扩容,理想情况下也算是比较合理的值。一段代码的IO操作越多,1核心对应的线程就要相应设置多点,CPU计算越多,1核心对应的线程就要相应设置少一些
  • 表明:默认的容量是16,也就是默认构造的实例,第一次扩容实际上是单线程执行的,看上去是可以多线程并发(方法允许多个线程进入),但是实际上其余的线程都会被一些if判断拦截掉,不会真正去执行扩容

MOVED/TREEBIN/RESERVED/HASH_BITS

  • MOVED:ForwardingNode的hash值,ForwardingNode是一种临时节点,在扩进行中才会出现,并且它不存储实际的数据。如果旧数组的一个hash桶中全部的节点都迁移到新数组中,旧数组就在这个hash桶中放置一个ForwardingNode。读操作或者迭代读时碰到ForwardingNode时,将操作转发到扩容后的新的table数组上去执行,写操作碰见它时,则尝试帮助扩容
  • TREEBIN:TreeBin的hash值,TreeBin是ConcurrentHashMap中用于代理操作TreeNode的特殊节点,持有存储实际数据的红黑树的根节点。因为红黑树进行写入操作,整个树的结构可能会有很大的变化,这个对读线程有很大的影响,所以TreeBin还要维护一个简单读写锁,这是相对HashMap,这个类新引入这种特殊节点的重要原因
  • RESERVED:ReservationNode的hash值,ReservationNode是一个保留节点,就是个占位符,不会保存实际的数据,正常情况是不会出现的,在jdk1.8新的函数式有关的两个方法computeIfAbsent和compute中才会出现
  • HASH_BITS:用于和负数hash值进行&运算,将符号位置为0,将其转化为正数(绝对值不相等),Hashtable中定位hash桶也有使用这种方式来进行负数转正数

NCPU

  • CPU的核心数,用于在扩容时计算一个线程一次要干多少活

serialPersistentFields

  • 在序列化时使用,这是为了兼容以前的版本

变量

Node<K,V>[] table

  • Node数组,用volatile修饰,通过Unsafe方法读写

Node<K,V>[] nextTable

  • 扩容后的新的table数组,只有在扩容时才有用
  • nextTable != null,说明扩容方法还没有真正退出,一般可以认为是此时还有线程正在进行扩容,极端情况需要考虑此时扩容操作只差最后给几个变量赋值(包括nextTable = null)的这个大的步骤,这个大步骤执行时,通过sizeCtl经过一些计算得出来的扩容线程的数量是0

long baseCount

  • 计数器基本值,主要在没有碰到多线程竞争时使用,需要通过CAS进行更新

int sizeCtl

  • 非常重要的一个属性,源码中的英文翻译,直译过来是下面的四行文字的意思
    • sizeCtl = -1,表示有线程正在进行真正的初始化操作
    • sizeCtl = -(1 + nThreads),表示有nThreads个线程正在进行扩容操作
    • sizeCtl > 0,表示接下来的真正的初始化操作中使用的容量,或者初始化/扩容完成后的threshold
    • sizeCtl = 0,默认值,此时在真正的初始化操作中使用默认容量
  • 但是,通过我对源码的理解,这段注释实际上是有问题的,有问题的是第二句,sizeCtl = -(1 + nThreads)这个,网上好多都是用第二句的直接翻译去解释代码,这样理解是错误的。默认构造的16个大小的ConcurrentHashMap,只有一个线程执行扩容时,sizeCtl = -2145714174,但是照这段英文注释的意思,sizeCtl的值应该是-(1 + 1) = -2,sizeCtl在小于0时的确有记录有多少个线程正在执行扩容任务的功能,但是不是这段英文注释说的那样直接用-(1 + nThreads),实际中使用了一种生成戳,根据生成戳算出一个基数,不同轮次的扩容操作的生成戳都是唯一的,来保证多次扩容之间不会交叉重叠,当有n个线程正在执行扩容时,sizeCtl在值变为(基数 + n),1.8.0_111的源码的383-384行写了个说明:A generation stamp in field sizeCtl ensures that resizings do not overlap.

int transferIndex

  • 下一个transfer任务的起始下标index加上1之后的值,transfer时下标index从length - 1开始往0走。transfer时方向是倒过来的,迭代时是下标从小往大,二者方向相反,尽量减少扩容时transefer和迭代两者同时处理一个hash桶的情况,顺序相反时,二者相遇过后,迭代没处理的都是已经transfer的hash桶,transfer没处理的,都是已经迭代的hash桶,冲突会变少。
  • 下标在[nextIndex - 实际的stride (下界要 >= 0), nextIndex - 1]内的hash桶,就是每个transfer的任务区间,每次接受一个transfer任务,都要CAS执行transferIndex = transferIndex - 实际的stride,保证一个transfer任务不会被几个线程同时获取(相当于任务队列的size减1),当没有线程正在执行transfer任务时,一定有transferIndex <= 0,这是判断是否需要帮助扩容的重要条件(相当于任务队列为空)

int cellsBusy

  • CAS自旋锁标志位,用于初始化,或者counterCells扩容时

CounterCell[] counterCells

  • 用于高并发的计数单元,如果初始化了这些计数单元,那么跟table数组一样,长度必须是2^n的形式

KeySetView<K,V> keySet/ValuesView<K,V> values/EntrySetView<K,V> entrySet

  • 视图变量

方法

静态方法

int spread(int h)

  • 将高16位与低16位异或,并将符号位置为0
  • 这样做是因为在计算node的index时,是用2的幂作为掩码,所以只用低位进行计算,存在大量的碰撞,比如一些Float的值,所以将高位的影响扩散到低位,可以减少这种碰撞。同时,因为table的容量限制,hash中的高位在计算index时很难被用到
  • 处于对速度、效能和bit位扩散的质量的考虑,并且使用红黑树处理大量的碰撞,所以只是简单的将高位和低位进行异或就够了

int tableSizeFor(int c)

  • 返回大于输入参数且最近的2的整数次幂的数
    先分析有关n位操作部分
        假设n的二进制为01xxx...xxx。接着
        对n右移1位:001xx...xxx,再位或:011xx...xxx
        对n右移2为:00011...xxx,再位或:01111...xxx
        此时前面已经有四个1了,再右移4位且位或可得8个1
        同理,有8个1,右移8位肯定会让后八位也为1
        综上可得,该算法让最高位的1后面的位全变为1
        最后再让结果n+1,即得到了2的整数次幂的值了
    
    现在回来看看第一条语句:int n = cap - 1
        让cap-1再赋值给n的目的是令找到的目标值大于或等于原值。例如二进制1000,十进制数值为8
        如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8
    

table元素访问方法

  • <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)
    • volatile语义获取table元素i
  • <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
    • CAS将位置i的元素c替换为v
  • <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
    • volatile语义将位置i的元素设置为v
  • 用Unsafe类实现这三个方法的原因是,Java的数组在元素层面上的设计缺失,无法表达元素是final和volatile的语义,所以使用getObjectVolatile补充volatile的语义,使用@Stable补充final的语义。数组元素本身和没有volatile修饰的字段一样,无法保证线程之间的可见性,只有触发happens-before关系的操作,才能保证线程之间的可见性。比如使用table[0] = new Object()直接赋值,这个赋值不会触发任何happens-before关系的操作,相当于对一个无volatile变量进行赋值一样

构造函数

  • ConcurrentHashMap()
  • ConcurrentHashMap(int initialCapacity)
  • ConcurrentHashMap(Map<? extends K, ? extends V> m)
  • ConcurrentHashMap(int initialCapacity, float loadFactor)
  • ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
    • long size = (long)(1.0 + (long)initialCapacity / loadFactor);initialCapacity * loadFactor = resizeThreshold,所以这里的initialCapacity / loadFactor表示乘以loadFactory后得到的resizeThreshold就是initialCapacity,即size达到initialCapacity后就会进行扩容,因此如果放到map中的元素数量刚好是initialCapacity个,那就避免了扩容操作,而+1.0相当于是使用Math.ceil将浮点数向上取整,不过如果initialCapacity/loadFactor是正数,就会多出一个元素,再用tableSizeFor调整size,就得到了最合适的capacity。比如initalCapacity=16,loadFactor=0.75,则size=22

get

源码

public V get(Object key) {
   
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 重hash
    int h = spread(key.hashCode());
    // 先看bin是否有结点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
   
        // bin的头结点就是要找的结点
        if ((eh = e.hash) == h) {
   
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // hash值小于0,都是特殊节点,调用find方法查询
        // 包括已经迁移的结点、树节点、临时节点
        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;
}

put

源码

final V putVal(K key, V value, boolean onlyIfAbsent) {
   
    if (key == null || value == null) throw new NullPointerException();
    // 重hash
    int hash = spread(key.hashCode());
    // 统计链表长度
    int binCount = 0;
    // 自旋
    for (Node<K,V>[] tab = table;;) {
   
        Node<K,V> f; int n, i, fh;
        // table为空则初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 如果bin没有结点,则直接CAS放入,失败则自旋重试
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
   
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 如果是fwd结点,表示正在迁移,则当前线程参与迁移table
        // 迁移结束后,自旋重试
        else if ((fh = f.hash) == MOVED
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值