为什么要使用ConcurrentHashMap
在多线程环境下,使用HashMap进行put操作,会引起死循环,导致CPU利用率接近100%。HashMap在并发执行put操作时会形成一种环形链表,一旦形成环形数据结构,Entry的nex节点不会为空,当查找一个链表中不存在的数据时,会引起死循环。
原因
在多并发情况家,一个线程先期完成了扩容,将原来的列表散列到自己的表中,形成了一个倒序,而另一个线程进行扩容时,将已倒序的表又散列成为正序,这样会产生一个环形,当get一个不存在的节点时,会产生一个死循环。所以在多线程环境下应该使用ConcurrentHashMap。
ConcurrentHashMap实现分析
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 是一种可重入锁(ReentrantLock),在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。Segment结构与HashEntry类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表元素。每个Segment元素守护HashEntry里边的元素。
构造方法初始化
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}
ConcurrentHashMap是通过initialCapacity,loadFactor和concurrencyLevel进行初始化,他们分别表示装载容量、负载因子和并发数量。其中concurrencyLevel 默认是DEFAULT_CONCURRENCY_LEVEL = 16;即容器里边分段锁的个数,也可以理解为默认并发度为16。segment的长度是通过concurrentyLevel计算得出的,segment的长度为2的N次方,所以必须计算出一个大于等于concurrentcyLevel的最小2的N次方来做Segment的长度。加入currentcylevel等于11或者12,则ssize等于12,即容器锁的个数也为12。
ConcurrentHashMap使用Segement分段锁来保护不同断的数据,在插入和获取元素之后都需要散列算法进行定位。ConcurrentHashMap会先使用Wang/Jenkins 变种算法进行散列。
get操作先使用hash算法定位Segment位置(使用了散列值的高位部分),再通过散列出的值再散列出值,定位HashEntry。
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
//定位锁
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//拿到到segment下的table
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
//开始遍历table下指定的Entry值
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
get方法没有加锁,其原因是HashEntry的value和next使用的是volatile修饰。保证每次取到的值都是最新值。
transient volatile HashEntry<K,V>[] table;
put操作
segment[0]在map初始化的时候已经被初始化。
// create segments and segments[0]
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;
ensureSegment函数,循环遍历,当有一个线程操作成功便跳出循环。
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
put会通过tryLock获得锁,如果成功获得所返回null,否则通过scanAndLockForPut自旋的方式获得锁,
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
scanAndLockForPut函数,通过自旋达到一定的次数还未获得锁,便调用locak阻塞式拿锁,在此过程中还不忘利用cpu的资源,创建一个node节点。
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
获得锁之后采用头插法,将元素插入到链表的head位置。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
rehash 操作
扩容是新创建了数组,然后进行迁移数据,最后再将 newTable 设置给属性table。
remove 操作
与 put 方法类似,都是在操作前需要拿到锁,以保证操作的线程安全性