HashMap的线程不安全性与FailFast机制

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的性能还是快一点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值