ConcurrentHashMap
学习了ArrayList与HashSet相对应的线程安全的CopyOnWriteArrayList 与CopyOnWriteArraySet之后,今天学习HashMpa对应的线程安全集合ConcurrentHashMap.
HashMap是线程不安全的,HashTable是线程安全的。但是HashTable是通过Synchronized进行同步的,这就导致一个线程占有锁,其它不管读写线程都需要等待,效率地下。
ConcurrentHashMap就解决这个问题。HashTable是把整个链表进行加锁,ConcurrentHashMap就是通过缩小锁的粒度来解决。
ConcurrentHashMap结构
从结构图中,对比,HashTable中维护的是一个链表,而在ConCurrentHashMap中,不是直接维护一个Hash链表,而是在中间加入了一个Segment(段)的概念。每个Segment去维护一个Hash链表。ConcurrentHashMap就是靠这个段来缩小锁的粒度。也就是说,ConcurrentHashMap把真个链表拆成多个Segment,由他们来各自维护一个链表,这样当要进行操作时,只需要操作对应的Segment就可以,而不需要像HashTable一样操作的是一个整体的链表。
优点
缩小的锁的粒度,这样大大增加了并发的能力
缺点
定位到具体的对象,需要进过两次Hash运算。第一次定位到段,再定位到链表头部。这样hash的过程要比HashTable长。
Segment
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile int count; //Segment中元素的数量
transient int modCount; //对table的大小造成影响的操作的数量
transient int threshold;//Segment扩容的阀值
transient volatile HashEntry<K,V>[] table;//链表数组,数组中的每一个元素代表了一个链表的头部
final float loadFactor;//负载因子
}
HashEntry
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;
}
}
hash和key都被定义为final类型,这样可以保证链表的机构不会出现被破坏的现象。
了解了段和链表的机构,就可以来看看ConcurrentHashMap是如何添加、删除、修改、获取的了。
get
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;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
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;
}
看上去挺复杂,其实就是第一步,通过hash key得到key的hash值,然后根据这个值与segmentShift和segmentMask进行运算,定位到对象在哪个段上面。
第二步,循环这个链表获取对应key的值。
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;
}
- 获取锁
- 定位到具体的HashEntry
- 遍历HashEntry链表,如果key已存在 再判断传入的onlyIfAbsent的值 ,再决定是否覆盖旧值.
- 释放锁,返回旧值.
remove
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;
}
先获取锁,然后定位到HashEntry,然后删除该元素,修改链表的节点链接。再以前的版本中的删除是,会把被删除元素的前面的元素都复制一遍,然后在删除之后在把复制的添加到链表中。因为此前版本中
volatile HashEntry<K,V> next;
HashEntry中的下一个节点都是被定义为final类型的。
size
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;
}
获取size其实可以理解为对每个段中的链表节点数量进行相加就行,但是由于存在并发问题,所以事先要获取每一个段的锁。然后进行相加。
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
这段代码就是获取段的锁。
注意jdk1.7与1.6的实现是不一样的具体详情可以参考:http://www.jianshu.com/p/bd972088a494