java中HashMap在多线程环境下引起CPU100%的问题解决

 最近项目中出现了Tomcat占用CPU100%的情况,原以为是代码中出现死循环,后台使用jstack做了dump,发现是系统中不合理使用HashMap导致出现了死循环(注意不是死锁)。

       产生这个死循环的根源在于对一个未保护的共享变量 — 一个"HashMap"数据结构的操作。当在所有操作的方法上加了"synchronized"后,一切恢复了正常。

       这算jvm的bug吗?应该说不是的,这个现象很早以前就报告出来了(详细见:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457)。Sun的工程师并不认为这是bug,而是建议在这样的场景下应采用"ConcurrentHashMap",

回复中的原话:

This is a classic symptom of an incorrectly synchronized use of
HashMap. Clearly, the submitters need to use a thread-safe
HashMap. If they upgraded to Java 5, they could just use ConcurrentHashMap. 

所以在开发过程中应当注意这点,在多线程的环境下,尽量使用ConcurrentHashMap。

可能出现问题的地方是在扩容的时候

Java代码  收藏代码
  1. void resize(int newCapacity) {    
  2.         Entry[] oldTable = table;    
  3.         int oldCapacity = oldTable.length;    
  4.         if (oldCapacity == MAXIMUM_CAPACITY) {    
  5.             threshold = Integer.MAX_VALUE;    
  6.             return;    
  7.         }    
  8.     
  9.         Entry[] newTable = new Entry[newCapacity];    
  10.         transfer(newTable);    
  11.         table = newTable;    
  12.         threshold = (int)(newCapacity * loadFactor);    
  13.     }    

 这个方法本身没有问题,问题出在transfer(newTable);这个方法是用来移动oldTable里的数据到newTable里。

Java代码  收藏代码
  1. /**  
  2.     * Transfers all entries from current table to newTable.  
  3.     */    
  4.    void transfer(Entry[] newTable) {    
  5.        Entry[] src = table;    
  6.        int newCapacity = newTable.length;    
  7.        for (int j = 0; j < src.length; j++) {    
  8.         //(1)    
  9.            Entry<K,V> e = src[j];    
  10.            if (e != null) {    
  11.                src[j] = null;    
  12.                do {    
  13.                 //(2)    
  14.                    Entry<K,V> next = e.next;    
  15.                    int i = indexFor(e.hash, newCapacity);    
  16.                 //(3)    
  17.                    e.next = newTable[i];    
  18.                    newTable[i] = e;    
  19.                    e = next;    
  20.                } while (e != null);    
  21.            }    
  22.        }    
  23.    }    

 下面分析可能出现的情况,假设原来oldTable里存放a1,a2的hash值是一样的,那么entry链表顺序是:

P1:oldTable[i]->a1->a2->null

P2:oldTable[i]->a1->a2->null

线程P1运行到(1)下面这行时,e=a1(a1.next=a2),继续运行到(2)下面时,next=a2。这个时候切换到线程P2,线程P2执行完这个链表的循环。如果恰a1,a2在新的table中的hash值又是一样的,那么此时的链表顺序是: 

主存:newTable[i]->a2->a1->null

注意这个时候,a1,a2连接顺序已经反了。现在cpu重新切回P1,在(3)这行以后:e.next = newTable[i];即:a1.next=newTable[i];

newTable[i]=a1;

e=a2;

开始第二次while循环(e=a2,next=a1):

a2.next=newTable[i];//也就是a2.next=a1

newTable[i]=a2

e=a1

开始第三次while循环(e=a1,next=null)

a1.next=newTable[i];//也就是a1.next=a2

这个时候a1.next=a2,a2.next=a1,形成回环了,这样就造成了死循环,在get操作的时候next永远不为null,造成死循环。

可以看到很偶然的情况下会出现死循环,不过一旦出现后果是非常严重的,多线程的环境还是应该用ConcurrentHashMap。

原文地址:https://www.cnblogs.com/gisblogs/p/4235948.html