HashMap在併發場景下踩過的坑

本文來自網易雲社區java



做者:張偉
web

關於HashMap在併發場景下的問題有不少人,不少公司遇到過!也不少人總結過,咱們不少時候都認爲這樣都坑距離本身很遠,本身必定不會掉入這樣都坑。但是咱們隨時都有就遇到了這樣都問題,坑一直都在咱們身邊。今天遇到了一個非線程安全對象在併發場景下使用的問題,經過這個案例分析HashMap 在併發場景下使用存在的問題(固然在這個案例中還有不少問題值得咱們去分析,值得你們引覺得戒。)經過分析問題產生都緣由,讓咱們從此更好遠離這個BUG。編程

 
 

    代碼如圖所示,你們都應該知道HashMap不是線程安全的。那麼   HashMap在併發場景下可能存在哪些問題?  安全

  1. 數據丟失併發

  2. 數據重複app

  3. 死循環源碼分析

  關於死循環的問題,在Java8中我的認爲是不存在了,在Java8以前的版本中之因此出現死循環是由於在resize的過程當中對鏈表進行了倒序處理;在Java8中再也不倒序處理,天然也不會出現死循環。this

     對這個問題Doug Lea 是這樣說的:spa

Doug Lea writes:

"This is a classic symptom of an incorrectly synchronized use ofHashMap. Clearly, the submitters need to use a thread-safe
HashMap. If they upgraded to Java 5, they could just useConcurrentHashMap. If they can't do this yet, they can use
either the pre-JSR166 version, or better, the unofficial backport
as mentioned by Martin. If they can't do any of these, they canuse Hashtable or synchhronizedMap wrappers, and live with poorer
performance. In any case, it's not a JDK or JVM bug."

I agree that the presence of a corrupted data structure alone
does not indicate a bug in the JDK.

 

   

       首先看一下put源碼    
   線程

public 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 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;
    } void addEntry(int hash, K key, V value, int bucketIndex) {        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }   
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

   
 經過上面Java7中的源碼分析一下爲何會出現數據丟失,若是有兩條線程同時執行到這條語句     table[i]=null,時兩個線程都會區建立Entry,這樣存入會出現數據丟失。  

若是有兩個線程同時發現本身都key不存在,而這兩個線程的key實際是相同的,在向鏈表中寫入的時候第一線程將e設置爲了本身的Entry,而第二個線程執行到了e.next,此時拿到的是最後一個節點,依然會將本身持有是數據插入到鏈表中,這樣就出現了數據 重複。經過商品put源碼能夠發現,是先將數據寫入到map中,再根據元素到個數再決定是否作resize.在resize過程當中還會出現一個更爲詭異都問題死循環。這個緣由主要是由於hashMap在resize過程當中對鏈表進行了一次倒序處理。假設兩個線程同時進行resize, 

A->B 第一線程在處理過程當中比較慢,第二個線程已經完成了倒序編程了B-A 那麼就出現了循環,B->A->B.這樣就出現了就會出現CPU使用率飆升。

 在下午忽然收到其中一臺機器CPU利用率不足告警,將jstack內容分析發現,可能出現了死循環和數據丟失狀況,固然對於鏈表的操做一樣存在問題。

PS:在這個過程當中能夠發現,之因此出現死循環,主要仍是在於對於鏈表對倒序處理,在Java 8中,已經不在使用倒序列表,死循環問題獲得了極大改善。

下圖是負載和CPU的表現:


下面是線程棧的部分日誌:

DubboServerHandler-10.172.75.33:20880-thread-139" daemon prio=10 tid=0x0000000004a93000 nid=0x76fe runnable [0x00007f0ddaf2d000]
   java.lang.Thread.State: RUNNABLE
	at java.util.HashMap.getEntry(HashMap.java:465)
	at java.util.HashMap.containsKey(HashMap.java:449)
	
"pool-9-thread-16" prio=10 tid=0x00000000033ef000 nid=0x4897 runnable [0x00007f0dd62cb000]
   java.lang.Thread.State: RUNNABLE
	at java.util.HashMap.put(HashMap.java:494)

DubboServerHandler-10.172.75.33:20880-thread-189" daemon prio=10 tid=0x00007f0de99df800 nid=0x7722 runnable [0x00007f0dd8b09000]
   java.lang.Thread.State: RUNNABLE
	at java.lang.Thread.yield(Native Method)
	

	DubboServerHandler-10.172.75.33:20880-thread-157" daemon prio=10 tid=0x00007f0de9a94800 nid=0x7705 runnable [0x00007f0dda826000]
   java.lang.Thread.State: RUNNABLE
	at java.lang.Thread.yield(Native Method)


網易雲大禮包:https://www.163yun.com/gift

本文來自網易實踐者社區,經做者張偉受權發佈


相關文章:
【推薦】 知物由學 | AI時代,那些黑客正在如何打磨他們的「利器」?

相關文章
相關標籤/搜索