了解Java并发集合ConcurrentHashMap
同Hashtable一样,ConcurrentHashMap也是一个线程安全的map结构集合,并且在多数情况下可以表现出更好的性能。因为ConcurrentHashMap使用了分段锁技术,将整个hash表分成了若干的segment,对其中一个segment加锁的的时候不会影响其他segment的访问。同时,ConcurrentHashMap提供了若一致性的迭代器,在使用迭代器遍历map的时候,允许其他线程插入或删除元素,真正做到了并发。
一,内存结构
ConcurrentHashMap的内存结构类似于多个HashMap的组合,内部维护了一个Segment数组。Segment继承了ReentrantLock,方便访问其内部数据时使用锁同步,其内部维护了一个HashEntry。HashEntry类似于HashMap中的Entry,只不过它内部的变量都是final或volatile的,更好的保证了内存可见性。
可以用一张图来表示ConcurrentHashMap的内存结构:
二,读写操作
ConcurrentHashMap的put操作比较复杂,Segment需要扩展时要先扩展Segment,HashEntry需要扩展时要先扩展HashEntry,同时还大量的使用了UNSAFE类。不考虑扩容的步骤大概是,先根据key的哈希值找到对应的Segment,然后找到对应的HashEntry,最后把这个元素加到这个HashEntry上。在获取Segment的时是不用加锁的:
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);
}
从put方法的代码中可以看到 ConcurrentHashMap的一个特性,它不允许值为null的value,当然也不允许null作为key。 put过程,到Segment内部的写操作才开始加锁。而读操作整个过程都不用加锁:
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;
}
get方法里将要使用的共享变量都定义成volatile,如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,保证线程写volatile变量后对其它线程立即可见。
三,若一致性的迭代器
非线程安全的集合类的迭代器都使用了fail-fast机制,在迭代的过程中发现集合发生了变化则抛出ConcurrentModificationException。然而,这只是一个善意的异常,并不能保证线程安全,要做到线程安全必须对这些类进行加锁。加锁后,可以保证集合的强一致性试图。但是在并发的情况下,不同线程之间的操作本来就是不相关的,谁先谁后对程序的结果没有影响,否则就要考虑通过加锁来控制各个线程的执行顺序。ConcurrentHashMap就提供了若一致性的迭代器,它允许在迭代的过程中其他线程对集合的修改。并发线程的修改有肯能影响迭代线程,可能影响不到,取决于它们读写共享变量的先后顺序。
ConcurrentHashMap中的迭代器都以HashIterator为父类,他的nextEntry()方法对应了迭代器的next()方法,其代码如下:
final void advance() {
for (;;) {
if (nextTableIndex >= 0) {
if ((nextEntry = entryAt(currentTable,
nextTableIndex--)) != null)
break;
}
else if (nextSegmentIndex >= 0) {
Segment<K,V> seg = segmentAt(segments, nextSegmentIndex--);
if (seg != null && (currentTable = seg.table) != null)
nextTableIndex = currentTable.length - 1;
}
else
break;
}
}
final HashEntry<K,V> nextEntry() {
HashEntry<K,V> e = nextEntry;
if (e == null)
throw new NoSuchElementException();
lastReturned = e; // cannot assign until after null check
if ((nextEntry = e.next) == null)
advance();
return e;
}
迭代器会逐个扫描每个Segment,每个HashEntry,每个 HashEntry的链式结构。若在迭代的过程中有元素的加入或删除,如果写操作发生在迭代器的读操作之前则可以发现此次的插入或删除动作,如果写操作发生在迭代器的读操作之后,则迭代器会忽略本次的插入或删除动作,当做没发生一样。
参考:
http://wiki.jikexueyuan.com/project/java-collection/concurrenthashmap.html