HashMap与ConcurrentHashMap
众所周知,HashMap无法做到线程安全,在某些特定的场景下甚至会出现死循环的情况。而ConcurrentHashMap是HashMap的并发版本,它能够做到线程安全。
ConcurrentHashMap返回的迭代器具有弱一致性(weakly consistent),而并非“及时失败”。弱一致性的迭代器可以容忍并发的修改…
——《Java并发编程实战》
迭代器的一致性问题
Java容器类的迭代操作最标准的方式就是使用Iterator,在并发修改的情况下,Iterator的一致性问题就暴露出来了。这种情况下,强一致性的表现就是”及时失败“,抛出大家并不陌生的ConcurrentModificationException。如果只支持弱一致性呢,它就会忽略这个不一致性,允许容器在迭代期间被并发修改。
强一致性是如何实现的
HashMap的强一致性是通过引入计数器实现的。
1 | /** |
通过引入一个名为modCount的计数器,当有任何修改容器的操作发生后,这个计数器就会自增。以put方法为例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
然后,我们再来看看HashMap中的迭代操作,以keySet()为例, 从代码中可以发现它所返回的迭代器类型为HashIterator,在HashIterator的初始化中,会将modCount赋值给expectedModCount, 如果在随后的迭代操作中发现两者不一致就会立即抛出ConcurrentModificationException,这也是所谓的fast-fail。
1 | public Set<K> keySet() { |
ConcurrentHash的迭代操作为什么是弱一致性的
同样的,ConcurrentHashMap的keySet()方法最终也是通过HashIterator的迭代器实现,但代码已经发生了变化,它已经不再维护任何计数器,而且代码中也不会再抛出ConcurrentHashMap(),当容器被并发修改时,迭代操作将会继续执行,因此无法保证迭代操作一定能够得到容器中最新的内容。
1 | abstract class HashIterator { |
总结
最后,从线程安全性、迭代操作的一致性和性能三个方面分别做一个比较:
Reference
- 《Java并发编程实战》
- jdk 1.7.0_45 源代码
订阅我的微信公众号,您将即时收到新博客提醒!