來源 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)用下面方法:
[
](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>

](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、刪除鍵值對的代碼
[
](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>

](javascript:void(0); "復(fù)制代碼")
當(dāng)多個線程同時操作同一個數(shù)組位置的時候,也都會先取得現(xiàn)在狀態(tài)下該位置存儲的頭結(jié)點,然后各自去進行計算操作,之后再把結(jié)果寫會到該數(shù)組位置去,其實寫回的時候可能其他的線程已經(jīng)就把這個位置給修改過了,就會覆蓋其他線程的修改。
3、addEntry中當(dāng)加入新的鍵值對后鍵值對總數(shù)量超過門限值的時候會調(diào)用一個resize操作,代碼如下:
[
](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>

](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線程安全?
[
](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>
[
](javascript:void(0); "復(fù)制代碼")
從源碼中看出 synchronizedMap()方法返回一個SynchronizedMap類的對象,而在SynchronizedMap類中使用了synchronized來保證對Map的操作是線程安全的,故效率其實也不高。