HashMap為什么是線程不安全的


HashMap底層是一個Entry數組,當發生hash沖突的時候,hashmap是采用鏈表的方式來解決的,在對應的數組位置存放鏈表的頭結點。對鏈表而言,新加入的節點會從頭結點加入。

我們來分析一下多線程訪問:

 1.在hashmap做put操作的時候會調用下面方法:

// 新增Entry。將“key-value”插入指定位置,bucketIndex是位置索引。      
    void addEntry(int hash, K key, V value, int bucketIndex) {      
        // 保存“bucketIndex”位置的值到“e”中      
        Entry<K,V> e = table[bucketIndex];      
        // 設置“bucketIndex”位置的元素為“新Entry”,      
        // 設置“e”為“新Entry的下一個節點”      
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);      
        // 若HashMap的實際大小 不小於 “閾值”,則調整HashMap的大小      
        if (size++ >= threshold)      
            resize(2 * table.length);      
    }  

 在hashmap做put操作的時候會調用到以上的方法。現在假如A線程和B線程同時對同一個數組位置調用addEntry,兩個線程會同時得到現在的頭結點,然后A寫入新的頭結點之后,B也寫入新的頭結點,那B的寫入操作就會覆蓋A的寫入操作造成A的寫入操作丟失

2.刪除鍵值對的代碼

<span style="font-size: 18px;">      </span>// 刪除“鍵為key”的元素      
    final Entry<K,V> removeEntryForKey(Object key) {      
        // 獲取哈希值。若key為null,則哈希值為0;否則調用hash()進行計算      
        int hash = (key == null) ? 0 : hash(key.hashCode());      
        int i = indexFor(hash, table.length);      
        Entry<K,V> prev = table[i];      
        Entry<K,V> e = prev;      
     
        // 刪除鏈表中“鍵為key”的元素      
        // 本質是“刪除單向鏈表中的節點”      
        while (e != null) {      
            Entry<K,V> next = e.next;      
            Object k;      
            if (e.hash == hash &&      
                ((k = e.key) == key || (key != null && key.equals(k)))) {      
                modCount++;      
                size--;      
                if (prev == e)      
                    table[i] = next;      
                else     
                    prev.next = next;      
                e.recordRemoval(this);      
                return e;      
            }      
            prev = e;      
            e = next;      
        }      
     
        return e;      
    }  

當多個線程同時操作同一個數組位置的時候,也都會先取得現在狀態下該位置存儲的頭結點,然后各自去進行計算操作,之后再把結果寫會到該數組位置去,其實寫回的時候可能其他的線程已經就把這個位置給修改過了,就會覆蓋其他線程的修改。

3.addEntry中當加入新的鍵值對后鍵值對總數量超過門限值的時候會調用一個resize操作,代碼如下:

// 重新調整HashMap的大小,newCapacity是調整后的容量      
    void resize(int newCapacity) {      
        Entry[] oldTable = table;      
        int oldCapacity = oldTable.length;     
        //如果就容量已經達到了最大值,則不能再擴容,直接返回    
        if (oldCapacity == MAXIMUM_CAPACITY) {      
            threshold = Integer.MAX_VALUE;      
            return;      
        }      
     
        // 新建一個HashMap,將“舊HashMap”的全部元素添加到“新HashMap”中,      
        // 然后,將“新HashMap”賦值給“舊HashMap”。      
        Entry[] newTable = new Entry[newCapacity];      
        transfer(newTable);      
        table = newTable;      
        threshold = (int)(newCapacity * loadFactor);      
    }  

 這個操作會新生成一個新的容量的數組,然后對原數組的所有鍵值對重新進行計算和寫入新的數組,之后指向新生成的數組。

      當多個線程同時檢測到總數量超過門限值的時候就會同時調用resize操作,各自生成新的數組並rehash后賦給該map底層的數組table,結果最終只有最后一個線程生成的新數組被賦給table變量,其他線程的均會丟失。而且當某些線程已經完成賦值而其他線程剛開始的時候,就會用已經被賦值的table作為原始數組,這樣也會有問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM