ConcurrentHashMap的JDK1.7源码解析(含详细注释)

为什么HashTable慢

  • HashTable 虽然它是一个线程安全的类,但是它是使用 synchronized 关键字来修饰一些操作。
  • synchronized 锁住了一整个哈希表。
  • 导致同一时期只有一个线程可以进行对哈希表的操作。
  • 所以 HashTable 虽然线程安全但是它慢,一个人操作所有人等待,并发值就是1。

ConcurrentHashMap

  • JDK1.5 ~JDK1.7 中,ConcurrentHashMap 这个类它使用的是分段锁的机制来实现并发控制的。

数据结构

  • 分段锁可以这么理解,我们原来的哈希表又在最外层套了一个数组,这个数组就被称作为 段(Segment)
  • 段数组中每个桶都包含了一个 HashMap 的哈希表,该哈希表在 JDK1.8 之前统一使用数组+链表的形式。
  • 段数组中的每个段都可以独立加锁,段的默认大小为 2^4 ,也就是默认可以有16个线程进行并发编程。
  • Segment 通过继承 ReentrantLock 来进行加锁。
  • 只要保证段中桶的线程安全,那么整个段也就是整个 ConcurrentHashMap 对象是线程安全的了。
  • 值得注意的是:Segment数组的长度是不可以被改变的,初始化如果不规定,那么就采用默认的 2^4

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d9OyrYIw-1643030217441)(C:\Users\noblegasesgoo\AppData\Roaming\Typora\typora-user-images\image-20211106094938292.png)]

初始化

  • 两个参数:
    • initialCapacity
      • 初始容量,这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment
      • 比如你初始容量是 64Segment 的容量为16,那么每个段中哈希表的初始容量就为 64/16=4
    • loadFactor
      • 这个负载因子是给 段中哈希表 扩容时候使用的。
  • 其中某个构造函数的源码详解:
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
   
    // 非法参数判断。
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    // 并行级别越界重设。
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    
    // Find power-of-two sizes best matching arguments 找到2次幂大小的最佳匹配参数.
    int sshift = 0; // 偏移量吧因该是。
    int ssize = 1; // segment的数组长度。
    
    // 计算并行级别 ssize,因为要保持并行级别是 2 的 n 次方
    // 如果这里我们的并行级别是16的话。
    while (ssize < concurrencyLevel) {
   
        ++sshift;
        ssize <<= 1;
    }
    // 到这一步,sshift = 4, ssize = 16。

    // 那么计算出 segmentShift 为 28,segmentMask 为 15,后面会用到这两个值
    this.segmentShift = 32 - sshift; // 段偏移量
    this.segmentMask = ssize - 1; // 段掩码

    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;

    // initialCapacity 是设置整个 map 初始的大小,
    // 这里根据 initialCapacity 计算 Segment 数组中每个位置可以分到的大小。
    // 如 initialCapacity 为 64,那么每个 Segment 或称之为"槽"可以分到 4 个。
    // 那么此时段中哈希表的容量就为 4。
    int c = initialCapacity / ssize; // c 用于
    
    // 如果段中容量与segment的数组长度乘积小于了整个 ConcurrentHashMap 的初始容量,
    // 那我们就把目标容量(段中哈希表容量) + 1;
    if (c * ssize < initialCapacity)
        ++c;
    
    // 默认 MIN_SEGMENT_TABLE_CAPACITY 是 2,这个值也是有讲究的,因为这样的话,对于具体的槽上,
    // 插入一个元素不至于扩容,插入第二个的时候才会扩容
    int cap = MIN_SEGMENT_TABLE_CAPACITY; //先给此时的段中容量默认为 2 。
    // 如果段中默认容量小于目标容量的话就一直扩大到段中默认容量等于目标容量。
    while (cap < c)
        cap <<= 1;

    // 创建段数组的第一个元素 segment[0]
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    // 创建段数组,数组长度就为 ssize 。
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    // 往段数组中有序的写入 segment[0]。
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

  • 如果是无参构造的话,我们可以从中看到段中哈希表的 默认大小为2,负载因子是0.75,那么 扩容阈值就是1.5,插入第一个元素不会扩容。
  • 那么到这整个 ConcurrentHashMap 对象就创建完毕了,但是有一个疑问:
    • 在末尾初始化了段表中的第一个位置,其他位置没有初始化,这是为什么呢?
    • 是因为每次创建一个 **Segment **对象要计算好几个值,初始化 **ConcurrentHashMap **的时候初始化了一个s0,只有再要初始化的 Segment 对象的时候,就拿s0当模板直接照搬参数就行,这样就会快一点。

put方法及其过程分析

  • 先来看看put方法的源码。
public V put(K key, V value) {
   
    // 先建一个段的临时桶。
    Segment<K,V> s;
    // 如果要put的值为空的话就抛出异常。
    if (value == null)
        throw new NullPointerException();
    
    // 计算 key 的 hash 值
    int hash = hash(key);
    
    // 根据 hash 值找到段表中的位置 j。
    // hash 是 32 位,无符号右移 segmentShift(28) 位,剩下高 4 位,
    // 然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的高 4 位,也就是槽的数组下标
    /*
    	0000 0000 0000 0000 0000 0000 0000 1101
    	0000 0000 0000 0000 0000 0000 0000 1111
    										&
    	----------------------------------------
    	0000 0000 0000 0000 0000 0000 0000 1101
    */
    
    int j = (hash >>> segmentShift) & segmentMask;
    // 刚刚说了,初始化的时候初始化了 segment[0],但是其他位置还是 null。
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensur
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值