1. HashMap底层存储的原理
HashMap底层的原理如下图:
其内部采用数组+链表的结构,每个数组对应一个bucket,一个bucket就是一个链表。当添加数据时,首先根据key计算得到hashcode,根据hashcode找到entry数组的位置,从该顶端位置往下遍历链表,如果发现某个其中某个node的key与待添加的key相同,则将该node的value替换,否则在链表的尾端新增一个node,存入待添加的key和value。
2. HashMap的线程不安全性
HashMap是线程不安全的,当多个线程并发操作时可能会发生最终操作不一致的情况,我们看下其添加元素的代码:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //通过hashcode找到数组的位置
tab[i] = newNode(hash, key, value, null); //如果该位置为空,则直接添加该node
else { //该位置不为空,则需要往下遍历
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //如果链表的头部node的key与待添加的key相同,则直接替换
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //往下遍历,一直找到尾部的node节点
p.next = newNode(hash, key, value, null); //添加尾部node节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //遍历过程中如果发现node的key与待添加的key相同,则直接替换该node
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //修改次数加1
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
从代码可以看出,添加元素时,是将元素添加到链表的尾部节点,而下面这行代码在并发时可能发生问题:
p.next = newNode(hash, key, value, null); //添加尾部node节点
如果线程1和线程2同时添加元素,而这2个元素的hashcode相同,则这2个元素应该会添加到同一个bucket中,假设这2个线程同时运行到该代码处,线程1首先执行完成,p.next指向新添加的node1,接着线程2执行完成,p.next又指向新添加的node2,这样线程1之前添加的node1就会丢失。
3. Fail-Fast机制
由于HashMap本身是线程不安全的,因此官方认为在对HashMap自身进行遍历时不应该改变其结构,包括添加和删除元素,而对其结构的改变都应该立刻抛出异常,防止继续遍历出现不可预料的问题,这就是Fail-Fast机制:
<p>The iterators returned by all of this class's "collection view methods"
* are <i>fail-fast</i>: if the map is structurally modified at any time after
* the iterator is created, in any way except through the iterator's own
* <tt>remove</tt> method, the iterator will throw a
* {@link ConcurrentModificationException}. Thus, in the face of concurrent
* modification, the iterator fails quickly and cleanly, rather than risking
* arbitrary, non-deterministic behavior at an undetermined time in the
* future.
来看下面的代码:
Map<String, String> map = new HashMap<>();
map.put("A", "a");
map.put("B", "b");
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
if ("A".equals(key)) {
map.remove(key);
}
}
运行时会抛异常:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1442)
at java.util.HashMap$EntryIterator.next(HashMap.java:1476)
at java.util.HashMap$EntryIterator.next(HashMap.java:1474)
at com.taotao.redis.JavaTest.main(JavaTest.java:14)
之所以会抛异常,是由于在遍历HashMap时进行了删除操作,改变了map的结构,因此触发了Fail-Fast机制
我们来看下Fail-Fast机制是如何实现的,首先看HashIterator的源码:
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
在迭代器遍历的过程中,会判断modCount和expectedModCount是否相等,若不相等则抛出异常,modCount是HashMap中的字段,记录了被修改的次数,但添加和删除元素时,该字段都会自增,而expectedModCount在Iterator初始化时被设置为和modCount相同,在修改HashMap的过程中该字段值不会变化,故当修改HashMap结构时会抛异常。
将示例代码作如下改动则运行不会报错
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
String key = entry.getKey();
if ("A".equals(key)) {
iterator.remove();
}
}
Iterator本身提供了remove方法:
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
从代码中可见,当删除节点之后,modCount自增,同时将modCount的值赋给expectedModCount,这样就保证了2个变量相等,在后续的迭代中不会抛异常。
4. 并发环境下解决方案
使用HashMap的Iterator在多线程环境下还是有问题,将代码改成下面的示例,则运行不会抛出异常
Map<String, String> map = new ConcurrentHashMap<>();
map.put("A", "a");
map.put("B", "b");
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
if ("A".equals(key)) {
map.remove(key);
}
}
上述代码中使用了ConcurrentHashMap,这是线程安全的,我们来看下其remove的代码:
可见,在remove的过程中,首先通过hashcode获取的bucket位置,然后对这个bucket进行加锁,因此ConcurrentHashMap的添加和删除都是通过加锁进行的,从而保证了并发操作的安全性。
ConcurrentHashMap虽然通过加锁操作保证了并发操作的安全性,但加锁解锁的操作会引起上下文切换,必然会消耗性能,因此在单线程环境下使用HashMap的性能还是快一点。