1、ConcurrentHashMap设计类图
ConcurrentHashMap是线程安全并且高效的HashMap,通过使用锁分段技术提升并发访问率。HashTable在竞争激烈的并发环境下效率低下的原因是所有访问HashTable的线程必须竞争同一把锁,如果容器有多把锁,每一把锁都锁容器中一部分数据,那么当访问容器中不同数据段时,线程间就不存在锁竞争,从而可以提高并发访问效率,这句是ConcurrentHashMap所使用的锁分段技术。
通过类图可以看到,ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment继承自RentrantLock,是一种可重入锁,扮演锁的角色;HashEntry则是用来存储键值对数据,与HashMap中Entry结构相似。一个ConcurrentHashMap里面组合一个Segment数组。Segment结构和HashMap类似,是一种数组和链表结构。一个Segment里面组合一个HashEntry数组,每个HashEntry结构是一个链表节点,各个Segment守护着HashEntry数组里面的元素,当对HashEntry数组进行修改时,必须先获得与他对应的Segment锁。
2、ConcurrentHashMap的构造实现以及重要方法分析
final Segment<K,V>[] segments;
ConcurrentHashMap中组合关联了一个Segment<K,V>数组。数组每一项可以看成是一分段HashMap,同时Segment锁守护这个分段。
1) 构造器
和HashMap一样,ConcurrentHashMap同样有initialCapacity和loadFactor属性,不过还多了一个concurrencyLevel属性,默认的这三个参数值分别为16,0.75和16。
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;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
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;
// 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;
}
当设置了initialCapacity、loadFactor和concurrencyLevel时,通过以下代码计算ssize值,决定Segment数组的大小。如果concurrencyLevel为16,则计算结果也是16,最终 会创建大小为16的Segment数组。
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
接着计算每个Segment里面包含的HashEntry数组大小,代码如下,最终的计算结果是2的整数幂大小,且大于等于
initialCapacity / ssize。
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
2)定位Segment
ConcurrentHashMap使用分段锁,那么在插入和获取元素时,必须先通过散列算法定位到Segment。可以看到ConcurrentHashMap使用
Wang/Jenkins
哈希算法。
private int hash(Object k) {
int h = hashSeed;
if ((0 != h) && (k instanceof String)) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// 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);
}
3)put(K key, V value): V 向map中添加元素的方法
//添加时,并没有在该方法上加上synchronized关键字,首先判断value
是否为null,若为null抛出异常。然后通过计算哈希值,确定其对应Segment数组的Segment对象。在找到Segment对象后,接着调用Segment的put方法。
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
当调用Segment的put方法时,首先进行lock操作,锁住当前Segment,接着判断当前存储的对象个数加1后是否大于threshold。如大于,则将当前的HashEntry对象数组大小扩大两倍,并将之前存储的对象重新hash,转移到新的数组中。接下来,通过哈希,得到key需要存放的位置,接着寻找对应位置上是否有相同的key,如果有,则替换。否则,创建一个HashEntry对象,链接到对应链表中。完成以上过程后,释放锁,整个put过程介绍。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
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;
}
}
} finally {
unlock();
}
return oldValue;
}
4) remove(Object key): V 移除元素方法
//移除时,首先对key进行哈希操作,基于得到的哈希值,寻找对相应的Segment对象,然后调用其remove方法进行操作。
public V remove(Object key) {
int hash = hash(key);
Segment<K,V> s = segmentForHash(hash);
return s == null ? null : s.remove(key, hash, null);
}
//在Segment的remove方法中,进行加锁操作,对hash值和对象数组大小减1的值进行按位与操作,获取到数组上对应位置的HashEntry对象,接着遍历寻找与key相同的元素,如果未找到,则返回null,否则,重建HashEntry链表中位于删除元素之前的所有HashEntry,位于其后的不做处理。最后释放锁。
final V remove(Object key, int hash, Object value) {
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
setEntryAt(tab, index, next);
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
5)size(): int 计算当前map大小的方法
前面的操作都是在单个Segment中进行的,但是ConcurrentHashMapsize的size操作是在多个Segment中进行。ConcurrentHashMap的size操作也采用了一种比较巧的方式,来尽量避免对所有的Segment都加锁。
前面我们提到了一个Segment中的有一个modCount变量,代表的是对Segment中元素的数量造成影响的操作的次数,这个值只增不减,size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历。
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}