并发容器(五) — ConcurrentHashMap 源码分析(JDK1.7)

一、概述

关联文章:

1.1 背景

HashMap(三) — 高并发场景下的问题分析 我们知道,在高并发下 HashMap是线程不安全的,主要原因是在同一个桶中并发操作会带来问题。为此我们只需要解决并发操作同一个桶的安全性问题即可,因此在SDK提供的ConcurrentHashMap类中通过分段锁(JDK1.7)或CAS+Sync(JDK1.8)来保证操作的安全性。

1.2 数据结构

  • JDK 1.7 HashMap 是通过 数组+链表来实现的,而ConcurrenthashMap在此基础上使用了分段锁来实现,即最终的数据结构为 Segment数组 + HashEntry数组 + HashEntry链表。
  • JDK 1.8 HashMap 是通过 数组+链表+红黑树来实现的,而ConcurrenthashMap在此基础上放弃了原有的分段锁机制,采用CAS+Sync同步块的方案来实现。

ConcurrenthashMap 的数据结构如下图所示:
在这里插入图片描述

二、预备知识

在开始分析源码前,先来了解下 Unsafe 和 Integer 类中几个方法的含义,方便后面理解。

  • Unsafe.arrayBaseOffset(Class<?> var) & Unsafe.arrayIndexScale(Class<?> var1)
  • Integer.numberOfLeadingZeros(int i) & 31 - Integer.numberOfLeadingZeros(int)

2.1 Unsafe类的几个方法

Unsafe.arrayBaseOffset(Class<?> var):返回数组中第一个元素的偏移地址。
Unsafe.arrayIndexScale(Class<?> var1):返回数组中一个元素占用的大小。

这两个方法通常是一起使用的,一个获取数组在内存中的起始地址baseAddress,一个获取数组中元素占用的内存大小objectSize ,通过这两个方法就可以对数组中任意一个元素进行操作 。例如数组中 第n个元素的地址 = baseAddress + (n * objectSize)

关于Unsafe类的详细解析可以参考:Java魔法类:Unsafe应用解析

2.2 Integer.numberOfLeadingZeros(int i) 方法

Integer.numberOfLeadingZeros(int i) 方法:

这个方法的作用是将 i 转为32位二进制后,计算首部0的个数(即从左边第一个位开始累加0的个数,直到遇到一个非零值)。
例如传入值为4,二进制为 4(十进制) = 00000000 00000000 00000000 00000100(二进制),返回值为29 (即前面有29个0)。

public static int numberOfLeadingZeros(int i) {
   
   
    // HD, Figure 5-6
    if (i == 0)
        return 32;
    int n = 1;
    if (i >>> 16 == 0) {
   
    n += 16; i <<= 16; }
    if (i >>> 24 == 0) {
   
    n +=  8; i <<=  8; }
    if (i >>> 28 == 0) {
   
    n +=  4; i <<=  4; }
    if (i >>> 30 == 0) {
   
    n +=  2; i <<=  2; }
    n -= i >>> 31;
    return n;
}

shift = 31 - Integer.numberOfLeadingZeros(int) 方法:

该方法一般结合 Unsafe.arrayIndexScale(Class<?> var1) 方法的返回值,将对象的大小转换为二进制的偏移量。

我们以实际的应用场景(AtomicIntegerArray类)来举例:

public class AtomicIntegerArray implements java.io.Serializable {
   
   
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // 返回数组中第一个元素的偏移地址
    private static final int base = unsafe.arrayBaseOffset(int[].class);
    private static final int shift;

    static {
   
   
    	// 计算出int数组中每个元素的大小(这里我们知道int占用4个字节)。
        int scale = unsafe.arrayIndexScale(int[].class);
        // 这里需要校验scale必须是2^n。
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        // 传入4后,Integer.numberOfLeadingZeros(4)返回29,所以shift=2。
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }
    
	// 计算第i个元素的内存地址。
    private static long byteOffset(int i) {
   
   
        return ((long) i << shift) + base;
    }
}

小结:

计算数组中指定位置元素的地址有两种方法。

  • 方案1:数组第i个元素地址 = base + i * scale 。
  • 方案2:数组第i个元素地址 = base + i << shift ;shift = 31 - Integer.numberOfLeadingZeros(scale)。

以具体的示例来说明:假设 int数组 在内存中的首个元素地址(base)为100,当操作第5个时,计算方法有两种:

  1. base + i * scale:第5个元素的内存地址=100 + 5*4(每个元素占4个字节) = 120。
  2. base + i << shift:第5个元素的内存地址=100 + 5<<2=120。


    为什么在执行 31 - Integer.numberOfLeadingZeros(scale) 前要校验 scale 的值是否是 2^n?
    还是以上面为例,假设 scale = 6,则上述两种方案计算出来的同一个位置元素的内存地址是不同的,因此使用第2中方案来计算数组中的元素内存地址是有限制条件的。


    为什么 31 - Integer.numberOfLeadingZeros(scale) 要使用 31 来减,而不使用32?
    一个int类型的数,最多只需要位移31次。

接下来我们将从源码角度来分析。


三、源码分析

JDK1.7版本中,我们主要分析如下几个方法:

  1. 静态代码块
  2. 构造方法
  3. ConcurrentHashMap.put(K key, V value) & Segment.put(K key, int hash, V value, boolean onlyIfAbsent)
  4. Segment.scanAndLockForPut(K key, int hash, V value)
  5. Segment.rehash(HashEntry<K,V> node) 扩容+数据迁移,最新添加的数据采用头插法。
  6. ConcurrentHashMap.get(Object key)
  7. ConcurrentHashMap.size()

3.1 静态代码块

通过 Unsafe 类获取数组相关信息。

static {
   
   
    int ss, ts;
    try {
   
   
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class tc = HashEntry[].class;
        Class sc = Segment[].class;
        // 计算出HashEntry[]&Segment[]的第一个元素的偏移地址
        TBASE = UNSAFE.arrayBaseOffset(tc);
        SBASE = UNSAFE.arrayBaseOffset(sc);
        // 计算出HashEntry[]&Segment[]中每个元素的大小
        ts = UNSAFE.arrayIndexScale(tc);
        ss = UNSAFE.arrayIndexScale(sc);
        //...
    } catch (Exception e) {
   
   
        throw new Error(e);
    }
    // 校验两个数组元素占用内存大小是否是2^n。
    if ((ss & (ss-1)) != 0 || (ts & (ts-1)) != 0)
        throw new Error("data type scale not a power of two");
    // 计算位运算时的偏移量
    SSHIFT = 31 - Integer.numberOfLeadingZeros(ss);
    TSHIFT = 31 - Integer.numberOfLeadingZeros(ts);
}

3.2 构造方法

// 用来计算索引的掩码值
final int segmentMask;
// 用来计算segments数组索引的位移量
final int segmentShift;

public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
   
   
    if (!(loadFactor > 0) || initialCapacity 
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值