為什么HashMap是線程不安全的,實際會如何體現(xiàn)?--轉(zhuǎn)發(fā)

來源 http://www.cnblogs.com/zlslch/p/7634819.html
 
首先,HashMap不支持線程的同步。

同步,指的是在一個時間點只能有一個線程可以修改hash表,任何線程在執(zhí)行Hashtable的更新操作前都需要獲取對象鎖,其他線程則等待鎖的釋放。

第一,如果多個線程同時使用put方法添加元素。

假設(shè)正好存在兩個put的key發(fā)生了碰撞(hash值一樣),那么根據(jù)HashMap的實現(xiàn),這兩個key會添加到數(shù)組的同一個位置,這樣最終就會發(fā)生其中一個線程的put的數(shù)據(jù)被覆蓋。

第二,如果多個線程同時檢測到元素個數(shù)超過數(shù)組大小*loadFactor。

這樣會發(fā)生多個線程同時對hash數(shù)組進行擴容,都在重新計算元素位置以及復(fù)制數(shù)據(jù),但是最終只有一個線程擴容后的數(shù)組會賦給table,也就是說其他線程的都會丟失,并且各自線程put的數(shù)據(jù)也丟失。且會引起死循環(huán)的錯誤。

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

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

1、在hashmap做put操作的時候會調(diào)用下面方法:

[
復(fù)制代碼

](javascript:void(0); "復(fù)制代碼")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">// 新增Entry。將“key-value”插入指定位置,bucketIndex是位置索引。
void addEntry(int hash, K key, V value, int bucketIndex) { // 保存“bucketIndex”位置的值到“e”中
Entry<K,V> e = table[bucketIndex]; // 設(shè)置“bucketIndex”位置的元素為“新Entry”, // 設(shè)置“e”為“新Entry的下一個節(jié)點”
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 若HashMap的實際大小 不小于 “閾值”,則調(diào)整HashMap的大小
if (size++ >= threshold)
resize(2 * table.length);
}</pre>

[
復(fù)制代碼

](javascript:void(0); "復(fù)制代碼")

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

2、刪除鍵值對的代碼

[
復(fù)制代碼

](javascript:void(0); "復(fù)制代碼")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"><span style="font-size: 18px;"> </span>// 刪除“鍵為key”的元素
final Entry<K,V> removeEntryForKey(Object key) { // 獲取哈希值。若key為null,則哈希值為0;否則調(diào)用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”的元素 // 本質(zhì)是“刪除單向鏈表中的節(jié)點”
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;
}</pre>

[
復(fù)制代碼

](javascript:void(0); "復(fù)制代碼")

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

3、addEntry中當(dāng)加入新的鍵值對后鍵值對總數(shù)量超過門限值的時候會調(diào)用一個resize操作,代碼如下:

[
復(fù)制代碼

](javascript:void(0); "復(fù)制代碼")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">// 重新調(diào)整HashMap的大小,newCapacity是調(diào)整后的容量
void resize(int newCapacity) {
Entry[] oldTable = table; int oldCapacity = oldTable.length; //如果就容量已經(jīng)達到了最大值,則不能再擴容,直接返回
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);
}</pre>

[
復(fù)制代碼

](javascript:void(0); "復(fù)制代碼")

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

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

如何實現(xiàn)HashMap的同步?

答:

 第一種方法:

   直接使用Hashtable,但是當(dāng)一個線程訪問HashTable的同步方法時,其他線程如果也要訪問同步方法,會被阻塞住。舉個例子,當(dāng)一個線程使用put方法時,另一個線程不但不可以使用put方法,連get方法都不可以,效率很低,現(xiàn)在基本不會選擇它了。

 第二種方法: HashMap可以通過下面的語句進行同步,

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">Collections.synchronizeMap(hashMap);</pre>

HashMap可以通過Map m = Collections.synchronizedMap(new HashMap())來達到同步的效果

具體而言,該方法返回一個同步的Map,該Map封裝了底層的HashMap的所有方法,使得底層的HashMap即使是在多線程的環(huán)境中也是安全的。

  第三種方法:

直接使用JDK 5 之后的 ConcurrentHashMap,如果使用Java 5或以上的話,請使用ConcurrentHashMap。

  Collections.synchronizeMap(hashMap); 又是如何保證了HashMap線程安全?

[
復(fù)制代碼

](javascript:void(0); "復(fù)制代碼")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">// synchronizedMap方法
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) { return new SynchronizedMap<>(m);
} // SynchronizedMap類
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable { private static final long serialVersionUID = 1978198479659022715L; private final Map<K,V> m; // Backing Map
final Object mutex; // Object on which to synchronize
SynchronizedMap(Map<K,V> m) { this.m = Objects.requireNonNull(m);
mutex = this;
}

   SynchronizedMap(Map<K,V> m, Object mutex) { this.m = m; this.mutex = mutex;
   } public int size() {
       synchronized (mutex) {return m.size();}
   } public boolean isEmpty() {
       synchronized (mutex) {return m.isEmpty();}
   } public boolean containsKey(Object key) {
       synchronized (mutex) {return m.containsKey(key);}
   } public boolean containsValue(Object value) {
       synchronized (mutex) {return m.containsValue(value);}
   } public V get(Object key) {
       synchronized (mutex) {return m.get(key);}
   } public V put(K key, V value) {
       synchronized (mutex) {return m.put(key, value);}
   } public V remove(Object key) {
       synchronized (mutex) {return m.remove(key);}
   } // 省略其他方法

}</pre>

[
復(fù)制代碼

](javascript:void(0); "復(fù)制代碼")

從源碼中看出 synchronizedMap()方法返回一個SynchronizedMap類的對象,而在SynchronizedMap類中使用了synchronized來保證對Map的操作是線程安全的,故效率其實也不高。

作者:大數(shù)據(jù)和人工智能躺過的坑
出處:http://www.cnblogs.com/zlslch/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、HashMap概述 HashMap基于哈希表的Map接口的實現(xiàn)。此實現(xiàn)提供所有可選的映射操作,并允許使用nul...
    小陳阿飛閱讀 674評論 0 2
  • 實際上,HashSet 和 HashMap 之間有很多相似之處,對于 HashSet 而言,系統(tǒng)采用 Hash 算...
    曹振華閱讀 2,561評論 1 37
  • Java集合:HashMap源碼剖析 一、HashMap概述 二、HashMap的數(shù)據(jù)結(jié)構(gòu) 三、HashMap源碼...
    記住時光閱讀 778評論 2 1
  • 摘要 HashMap是Java程序員使用頻率最高的用于映射(鍵值對)處理的數(shù)據(jù)類型。隨著JDK(Java Deve...
    周二倩你一生閱讀 1,357評論 0 5
  • 工作得久了,就想出去走走。外面的世界豈止精彩,于我而言,簡直是意氣風(fēng)發(fā)與豆蔻年華的雙重治愈。渴望遠(yuǎn)行,渴望在新鮮的...
    只有桂花香暗飄過閱讀 400評論 0 1

友情鏈接更多精彩內(nèi)容