ConcurrentHashMap是支持多线程并发操作的哈希表,与HashTable相似,不支持null的key或value,方法声明上也遵循了HashTable的规范。总体数据结构与HashMap类似,都是数组,按“链地址法”哈希具体的值。但ConcurrentHashMap内部按“段”来组织,每个段对应了一个或多个哈希entry。写操作(put,remove等)都需要加排它锁,而读操作(get)不需要加锁,因此获取的值可能是读操作的中间状态,尤其对(putAll和clear),读操作可能只能获取部分值。迭代器和enumeration返回的是哈希表某个状态,不抛出ConcurrentModificationException。迭代器同一时刻只允许一个线程使用。
ConcurrentHashMap的成员变量有:
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
static final int MAX_SEGMENTS = 1 << 16;
static final int RETRIES_BEFORE_LOCK = 2;
其中大部分与HashMap一致。DEFAULT_CONCURRENCY_LEVEL一定程度上衡量了可支持的并发线程数;MIN_SEGMENT_TABLE_CAPACITY是每个段最少的哈希entry,即每个段中HashEntry数组最小容量;MAX_SEGMENTS是最大允许的段数目(源码中注释说是不小于1<<24,但默认的值给的是1<<16,并且对该值注释说”slightly conservative”);RETRIES_BEFORE_LOCK是size和containsValue方法加锁时重试的最大次数,如果在多次重试后仍没有获取锁,则线程进入中断状态在加锁队列中排队。
简单来说,ConcurrentHashMap是一个Segment<K,V>[]数组,每个Segment<K,V>又包含一个HashEntry<K,V>[]数组(即维护了一个小的hash表),HashEntry数组每个元素就是一个hash值对应的HashEntry链,具体的key-value对就放在这个链中。
先来说说Segment<K,V>。这是一个内部类,派生自ReentrantLock。主要的成员变量包括:
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
其中,loadFactor和threshold与HashMap中一致;table就是该段一个小的hash表;count是表中的元素个数;modCount是段中所有可变操作(put,remove,clear等)的次数,在isEmpty和size中用得到,如果值溢出可能会导致一些问题。
段的主要操作是put,remove,replace,clear。除了clear,每次操作都需要tryLock,如果未获得锁,则调用scanAndLock或scanAndLockForPut,一方面继续请求获得独占锁,另一方面检索表以找到待操作的节点(The main benefit is to absorb cache misses (which are very common for hash tables) while obtaining locks so that traversal is faster once)。如果多次(最大为MAX_SCAN_RETRIES)请求都没能获得锁,则interrupt线程,进入队列等待直到获得锁。具体细节与ReentrantLock的实现有关。相比而言,clear操作就显得粗暴一点,直接调用lock加锁。此外,put可能导致rehash,也是以两倍的大小增长。
再来看看ConcurrentHashMap自己的成员。ConcurrentHashMap最关键的构造函数:
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel)
首先找到一个不小于concurrentcyLevel的数,这个数即ssize必须是2的幂,且2^sshift=ssize,由此得到segmentShift和segmentMask。
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
再看cap。cap是每个段中的表初始大小,cap也是2的幂,最小是2。cap×sszie>=initialCapacity。
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;
最后,创建段数组。s0好比一个段模板,初始化表大小为cap,rehash的threshold为cap×loadFactor,根据s0创建段数组。
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;
因此,concurrentHashMap有ssize个段,即表示最多支持ssize个线程同时写。
concurrentHashMap的方法中,很多都有一个“recheck”的过程。
如isEmpty()方法,先检查每个段的count,如果有不为0直接返回false,否则累加modCount得到sum;接着再遍历一遍段,如果count有不为0则返回false,否则用之前的sum递减modCount,若最后sum不为0则返回false,否则为true。这样做是针对一个段进行检查的同时另一个段正在进行修改的情况,意义和作用我还不是特别能理解。isEmpty并不需要加锁。
Size()操作都需要对每个段进行加锁(也不一定,如果发现为空则直接返回0);
containsValue和size一样,在指定次数(RETRIES_BEFORE_LOCK)内没发现想要的值,则强制加锁进行排它性查找,如果找到则返回true;还有个contains,是为了保持与HashTable一致,实际调用的就是containsValue。
get和containsKey都不需要加锁,表面含义和代码都很容易理解,如果参数为null则抛出NullPointerException。
put操作首先找到段数组中指定映射的段,没有则生成一个新的段,接着调用段的put方法插入数据,每次在链头部插入。
putIfAbsent与put差不多,不同的是只有在不存在指定key时才会插入新的key-value值,如果已经有了这个key则不执行更新。
putAll调用put进行插入;
remove调用segment的remove;
replace调用segment的replace;
clear依次调用每个段的clear。clear并不会同时对所有段加锁,因此可能在执行clear操作时,有读操作发生会出现读取到clear操作的中间结果,putAll也有这种情况。
keySet返回一个key的set,该set有个弱一致性的迭代器,不抛出ConcurrentModificationException。迭代器的操作调用的是该concurrentHashMap相关的方法,包括remove,size,contains,clear,isEmpty等,不包括任何put操作。所有iterator上的改动都会直接反应到ConcurrentHashMap,反之亦然。
values,entrySet同keySet;elements和keys返回的是一个枚举集。
再说hash算法,HashMap的hash算法已经够变态了,没想到一山还有一山高,ConcurrentHashMap也是特么的吓唬人啊。
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
最后,ConcurrentHashMap的实现比起HashMap复杂很多,还有很多概念我理解的还很不到位,例如volatile的用法,“happen-before”到底是什么意思?不加锁的读会出现什么可能意想不到的后果?等等。