最近遇到併發往HahsMap裏put的情況,遇到相同的問題,分析很好,也懶得自己寫了,傳播一下別人的只是。
==============================
當你明明put進了一對非null key-value進了HashMap,某個時候你再用這個key去取的時候卻發現value爲null,再次取的時候卻又沒問題,都知道是HashMap的非線程安全特性引起的,分析具體原因如下:
Java代碼
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
// indexFor方法取得key在table數組中的索引,table數組中的元素是一個鏈表結構,遍歷鏈表,取得對應key的value
for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
再看看put方法:
Java代碼
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
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++;
// 若之前沒有put進該key,則調用該方法
addEntry(hash, key, value, i);
return null;
再看看addEntry裏面的實現:
Java代碼
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
裏面有一個if塊,當map中元素的個數(確切的說是元素的個數-1)大於或等於容量與加載因子的積時,裏面的resize是就會被執行到的,繼續resize方法:
Java代碼
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int) (newCapacity * loadFactor);
resize裏面重新new一個Entry數組,其容量就是舊容量的2倍,這時候,需要重新根據hash方法將舊數組分佈到新的數組中,也就是其中的transfer方法:
Java代碼
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry e = src[j];
if (e != null) {
src[j] = null;
do {
Entry next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
在這個方法裏,將舊數組賦值給src,遍歷src,當src的元素非null時,就將src中的該元素置null,即將舊數組中的元素置null了,也就是這一句:
Java代碼
if (e != null) {
src[j] = null;
此時若有get方法訪問這個key,它取得的還是舊數組,當然就取不到其對應的value了。
下面,我們重現一下場景:
Java代碼
import java.util.HashMap;
import java.util.Map;
public class TestHashMap {
public static void main(String[] args) {
final Map map = new HashMap(4, 0.5f);
new Thread(){
public void run() {
while(true) {
System.out.println(map.get("name1"));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
for(int i=0; i<3; i++) {
map.put("name" + i, "value" + i);
}
Debug上面這段程序,在map.put處設置斷點,然後跟進put方法中,當i=2的時候就會發生resize操作,在transfer將元素置null處停留片刻,此時線程打印的值就變成null了。
其它可能由未同步HashMap導致的問題:
1、多線程put後可能導致get死循環(主要問題在於put的時候transfer方法循環將舊數組中的鏈表移動到新數組)
2、多線程put的時候可能導致元素丟失(主要問題出在addEntry方法的new Entry(hash, key, value,
e),如果兩個線程都同時取得了e,則他們下一個元素都是e,然後賦值給table元素的時候有一個成功有一個丟失)
總結:HashMap在併發程序中會產生許多微妙的問題,難以從表層找到原因。所以使用HashMap出現了違反直覺的現象,那麼可能就是併發導致的了