昨天寫了一個多線程的程序,卻發現了一個很奇特的問題,就是我的map對象明明put了,可是get的時候竟然會取到null,而且嘗試多次,有時候成功,有時候取到null,並不確定。
程序代碼如下:
public class ThreadLocal { private static Map<Thread, Integer> map; public static void main(String[] args) { map = new HashMap<Thread, Integer>(); for (int i = 0; i < 2; i++) { new Thread(new Runnable() { public void run() { int data = new Random().nextInt(); map.put(Thread.currentThread(), data); System.out.println(Thread.currentThread() + ", data:" + data); new A().show(); new B().show(); } }).start(); } } static class A { public void show() { System.out.println(Thread.currentThread() + "調用A, data:" + map.get(Thread.currentThread())); } } static class B { public void show() { System.out.println(Thread.currentThread() + "調用B, data:" + map.get(Thread.currentThread())); } } }
運行結果如下:
Thread[Thread-0,5,main], data:1164116165
Thread[Thread-1,5,main], data:196549485
Thread[Thread-0,5,main]調用A, data:null
Thread[Thread-1,5,main]調用A, data:196549485
Thread[Thread-0,5,main]調用B, data:null
Thread[Thread-1,5,main]調用B, data:196549485
我實在想不明白,我明明已經put了,為什么取不到呢?今天我查了資料,並大概看了看源碼,大致理解了,這是由HashMap的非線程安全特性引起的。具體源碼分析如下所示:
先看看get方法:
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方法:
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里面的實現:
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); }
map里的元素個數(size)大於一個閾值(threshold)時,map將自動擴容,容量擴大到原來的2倍;
閾值(threshold)是怎么計算的?如下源碼:
threshold = (int)(capacity * loadFactor);
閾值 = 容量 X 負載因子;容量默認為16,負載因子(loadFactor)默認是0.75; map擴容后,要重新計算閾值;當元素個數大於新的閾值時,map再自動擴容;
以默認值為例,閾值=16*0.75=12,當元素個數大於12時就要擴容;那剩下的4(如果內部形成了Entry鏈則大於4)個數組位置還沒有放置對象就要擴容,豈不是浪費空間了?
這是時間和空間的折中考慮;loadFactor過大時,map內的數組使用率高了,內部極有可能形成Entry鏈,影響查找速度;loadFactor過小時,map內的數組使用率舊低,不過內部不會生成Entry鏈,或者生成的Entry鏈很短,由此提高了查找速度,不過會占用更多的內存;所以可以根據實際硬件環境和程序的運行狀態來調節loadFactor。
繼續resize方法:
void resize(int newCapacity) { //傳入新的容量
Entry[] oldTable = table; //引用擴容前的Entry數組
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) { //擴容前的數組大小如果已經達到最大(2^30)了
threshold = Integer.MAX_VALUE; //修改閾值為int的最大值(2^31-1),這樣以后就不會擴容了
return;
}
Entry[] newTable = new Entry[newCapacity]; //初始化一個新的Entry數組
transfer(newTable); //!!將數據轉移到新的Entry數組里
table = newTable; //HashMap的table屬性引用新的Entry數組
threshold = (int) (newCapacity * loadFactor); //修改閾值
}
resize里面重新new一個Entry數組,其容量就是舊容量的2倍,這時候,需要重新根據hash方法將舊數組分布到新的數組中,也就是其中的transfer方法:
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了舊的Entry數組
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) { //遍歷舊的Entry數組
Entry e = src[j]; //取得舊Entry數組的每個元素
if (e != null) {
src[j] = null; //釋放舊Entry數組的對象引用(for循環后,舊的Entry數組不再引用任何對象) do {
Entry next = e.next;
int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在數組中的位置
e.next = newTable[i]; //標記[1]
newTable[i] = e; //將元素放在數組上
e = next; //訪問下一個Entry鏈上的元素
} while (e != null);
}
}
}
indexFor()是計算每個元素在數組中的位置,源碼:
return h & (length-1); //位AND計算
}
例如,舊數組容量為16,對象A的hash值是4,對象B的hash值是20,對象C的hash值是36;
通過indexFor()計算后,A、B、C對應的數組索引位置分別為4,4,4; 說明這3個對象在數組的同一位置上,形成了Entry鏈;
舊數組擴容后容量為16*2,重新計算對象所在的位置索引,A、B、C對應的數組索引位置分別為4,20,4; B對象已經被放到別處了;
所以,resize時,HashMap使用新數組代替舊數組,對原有的元素根據hash值重新就算索引位置,重新安放所有對象;resize是耗時的操作。
