历史文章推荐:
一些题外话
前一篇文章我们对HashMap
的实现做了详细的解析和总结,这篇文章继续剖析一下ConcurrentHashMap
的实现。由于ConcurrentHashMap
的内容比较多,而且Java 7
和Java 8
两个版本的实现相差比较大,如果采用我们上篇中对比的那种行文思路,在有限的篇幅中难免会遗漏一些细节,因此我决定采用两篇文章去详细阐述两个版本中ConcurrentHashMap
技术细节,不过为了帮助读者体系化的理解,三篇文章(包含HashMap
的那一篇)整体文章的结构将保持一致。
把书读薄
《阿里巴巴Java开发手册》的作者孤尽对ConcurrentHashMap
的设计十分推崇,他说:“ConcurrentHashMap
源码是学习Java
代码开发规范的一个非常好的学习材料,我建议同学们可以时常去看一看,总会有新的收货的”,相信大家平常也能听到很多对于ConcurrentHashMap
设计的溢美之词,在展开隐藏在ConcurrentHashMap
所有小秘密之前,大家在大脑中首先要有这样的一幅图:
对于Java 7
来说,这张图已经能完全把ConcurrentHashMap
的架构说清楚了:
ConcurrentHashMap
是一个线程安全的Map
实现,其读取不需要加锁,通过引入Segment
,可以做到写入的时候加锁力度足够小- 由于引入了
Segment
,ConcurrentHashMap
在读取和写入的时候需要需要做两次哈希,但这两次哈希换来的是更小的临界区,也就意味着可以支持更高的并发 - 每个桶数组中的
key-value
对仍然以链表的形式存放在桶中,这一点和HashMap
是一致的。
把书读厚
关于Java 7
的ConcurrentHashMap
的整体架构,用上面三两句话就可以概括,这张图应该很快就可以在大家的大脑中留下印象,接下来我们通过几个问题来尝试吸引大家继续看下去,把书读厚:
ConcurrentHashMap
的哪些操作需要加锁?ConcurrentHashMap
的无锁读是如何实现的?- 在多线程的场景下调用
size()
方法获取ConcurrentHashMap
的大小有什么挑战?ConcurrentHashMap
是怎么解决的? - 在有
Segment
存在的前提下,是如何扩容的?
在上一篇文章中我们总结了HashMap
中最重要的点有四个:初始化,数据寻址-hash
方法,数据存储-put
方法,扩容-resize
方法,对于ConcurrentHashMap
来说,这四个操作依然是最重要的,但由于其引入了更复杂的数据结构,因此在调用size()
查看整个ConcurrentHashMap
的数量大小的时候也有不小的挑战,我们也会重点看下Doug Lea在size()
方法中的设计
初始化
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
int sshift = 0;
int ssize = 1;
// 保证ssize是大于concurrencyLevel的最小的2的整数次幂
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// 寻址需要两次哈希,哈希的高位用于确定segment,低位用户确定桶数组中的元素
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
初始化方法中做了三件重要的事:
- 确定了
segments
的数组的大小ssize
,ssize
根据入参concurrencyLevel
确定,取大于concurrencyLevel
的最小的2的整数次幂 - 确定哈希寻址时的偏移量,这个偏移量在确定元素在
segment
数组中的位置时会用到 - 初始化
segment
数组中的第一个元素,元素类型为HashEntry
的数组,这个数组的长度为initialCapacity / ssize
,即初始化大小除以segment
数组的大小,segment
数组中的其他元素在后续put
操作时参考第一个已初始化的实例初始化
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
final void setNext(HashEntry<K,V> n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
}
这里的HashEntry
和HashMap
中的HashEntry
作用是一样的,它是ConcurrentHashMap
的数据项,这里要注意两个细节:
细节一:
HashEntry
的成员变量value
和next
是被关键字volatile<