HashMap爲何是線程不安全的

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

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

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

// 新增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的寫入操做丟失spa

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;      
    }  

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

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

// 從新調整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做爲原始數組,這樣也會有問題。ci

相關文章
相關標籤/搜索