一、概述
关联文章:
- HashMap(一) — HashMap 源码分析
- HashMap(二) — 浅析hash函数及tableSizeFor函数
- HashMap(三) — 高并发场景下的问题分析
- 可重入锁(ReentrantLock)源码解析
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个时,计算方法有两种:
- base + i * scale:第5个元素的内存地址=100 + 5*4(每个元素占4个字节) = 120。
- 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版本中,我们主要分析如下几个方法:
- 静态代码块
- 构造方法
ConcurrentHashMap.put(K key, V value)&Segment.put(K key, int hash, V value, boolean onlyIfAbsent)Segment.scanAndLockForPut(K key, int hash, V value)Segment.rehash(HashEntry<K,V> node)扩容+数据迁移,最新添加的数据采用头插法。ConcurrentHashMap.get(Object key)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

最低0.47元/天 解锁文章
187

被折叠的 条评论
为什么被折叠?



