• 高并发下,HashMap会产生哪些问题?


    HashMap在高并发环境下会产生的问题

    HashMap其实并不是线程安全的,在高并发的情况下,会产生并发引起的问题:
    比如:

    • HashMap死循环,造成CPU100%负载
    • 触发fail-fast

    下面逐个分析下出现上述情况的原因:

    HashMap死循环的原因

    HashMap进行存储时,如果size超过(当前最大容量*负载因子)时候会发生resize,首先看一下resize源代码:

    void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
     
            Entry[] newTable = new Entry[newCapacity];
            // transfer方法是真正执行rehash的操作,容易在高并发时发生问题
            transfer(newTable);
            table = newTable;
            threshold = (int)(newCapacity * loadFactor);
        }
    

    而这段代码中又调用了transfer()方法,而这个方法实现的机制就是将每个链表转化到新链表,并且链表中的位置发生反转,而这在多线程情况下是很容易造成链表回路,从而发生死循环,我们看一下他的源代码:

    void transfer(Entry[] newTable) {
            Entry[] src = table;
            int newCapacity = newTable.length;
            for (int j = 0; j < src.length; j++) {
                Entry<K,V> e = src[j];
                if (e != null) {
                    src[j] = null;
                    do {
                        Entry<K,V> next = e.next;
                        int i = indexFor(e.hash, newCapacity);
                        e.next = newTable[i];
                        newTable[i] = e;
                        e = next;
                    } while (e != null);
                }
            }
        }
    

    HashMap死循环演示:
    假如有两个线程P1、P2,以及table[]某个节点链表为 a->b->null(a、b是HashMap的Entry节点,保存着Key-Value键值对的值)

    1. P1先执行,执行完"Entry<K,V> next = e.next;"代码后,P1发生阻塞或者其他情况不再执行下去,此时e=a,next=b

    2. P1阻塞后P2获得CPU资源开始执行,由于P1并没有执行完transfer(),table 和 threshold仍为原来的值,P2依旧会进行resize操作,并且P2顺利执行完resize()方法,假设a、b节点仍然rehash到newTable[](注意,P1和P2中newTable[]不是同一个)中同一个节点链表中,则新的节点链表为 b->a->null

          transfer(newTable);   //P1阻塞在transfer方法中,没有执行到下边对 table 和 threshold 重新赋值的操作
          table = newTable;
          threshold = (int)(newCapacity * loadFactor);
      
    3. P1又继续执行"Entry<K,V> next = e.next;"之后的代码,则newTable[i]的节点链表变化过程为:

      • 第一次while循环,newTable[i]=a,链表为:b->a->null;此时e=b;
      • 进入第二次循环,newTable[i]=b,链表为:b->a->a; 此时a<->a出现回路,e=a, while(e!=null)死循环

    触发fail-fast

    一个线程利用迭代器迭代时,另一个线程做插入删除操作,造成迭代的fast-fail。

    public class TestFailFast {
        
        private static final String USER_NAME_PREFIX = "User-";
        // Key: User Name, Value: User Age
        private static Map<String, Integer> userMap = new HashMap<>();
        
        // ThreadA 用于向HashMap添加元素
        static class ThreadA implements Runnable {
            @Override
            public void run() {
                System.out.println("ThreadA starts to add user.");
                for (int i = 1; i < 100000; i++) {
                    userMap.put(USER_NAME_PREFIX+i, i%100);
                }
                System.out.println("ThreadA done.");
            }
        }
        
        // ThreadB 用于遍历HashMap中元素输出
        static class ThreadB implements Runnable {
            @Override
            public void run() {
                System.out.println("ThreadB starts to iterate.");
                for (Map.Entry<String, Integer> user : userMap.entrySet()) {
                    System.out.println("UserName=" + user.getKey()
                        + ", UserAge=" + user.getValue());
                }
                System.out.println("ThreadB done.");
            }
        }
        
        public static void main(String[] args) throws InterruptedException {
            Thread threadA = new Thread(new ThreadA());
            Thread threadB = new Thread(new ThreadB());
        
            threadA.start();
            threadB.start();
      
            threadA.join();
            threadB.join();
            System.exit(0);
        }
    }
    

    运行结果:抛出ConcurrentModificationException

    ThreadA starts to add user.
    ThreadB starts to iterate.
    Exception in thread "Thread-1" java.util.ConcurrentModificationException
    	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
    	at java.util.HashMap$EntryIterator.next(HashMap.java:1471)
    	at java.util.HashMap$EntryIterator.next(HashMap.java:1469)
    	at concurrent.TestFailFast$ThreadB.run(TestFailFast.java:33)
    	at java.lang.Thread.run(Thread.java:748)
    ThreadA done.
    

    总结

    HashMap并非线程安全,所以在多线程情况下,应该首先考虑用ConcurrentHashMap,避免悲剧的发生。

    参考资料:

    https://blog.csdn.net/chenxuegui1234/article/details/39646041
    https://blog.csdn.net/u011716215/article/details/78601916

  • 相关阅读:
    Open source physics engine
    Free Platformers: Open Source Gamers Guide to Free Games
    安装路由后,显示已连接,却上不了网?
    http://blog.csdn.net/duanbeibei/article/details/5890436
    javascript权威指南 第8章 笔记2 Kevin
    javascript权威指南 第9章 笔记 Kevin
    javascript权威指南 笔记2 Kevin
    .Net 登录窗口 Kevin
    C# 中读XML时haschrildnodes方法老为true Kevin
    javascript权威指南 第8章 笔记 Kevin
  • 原文地址:https://www.cnblogs.com/lanqiu5ge/p/9606645.html
Copyright © 2020-2023  润新知