并發實踐之一:HashMap的非線程安全性和ConcurrentHasMap
Posted on 2010-09-05 17:12 kangaroo 閱讀(12204) 評論(5) 編輯 收藏 所屬分類: tech/多線程在平時開發中,我們經常采用HashMap來作為本地緩存的一種實現方式,將一些如系統變量等數據量比較少的參數保存在HashMap中,并將其作為單例類的一個屬性。在系統運行中,使用到這些緩存數據,都可以直接從該單例中獲取該屬性集合。但是,最近發現,HashMap并不是線程安全的,如果你的單例類沒有做代碼同步或對象鎖的控制,就可能出現異常。
首先看下在多線程的訪問下,非現場安全的HashMap的表現如何,在網上看了一些資料,自己也做了一下測試:
1
public class MainClass {
2
3
public static final HashMap<String, String> firstHashMap=new HashMap<String, String>();
4
5
public static void main(String[] args) throws InterruptedException {
6
7
//線程一
8
Thread t1=new Thread(){
9
public void run() {
10
for(int i=0;i<25;i++){
11
firstHashMap.put(String.valueOf(i), String.valueOf(i));
12
}
13
}
14
};
15
16
//線程二
17
Thread t2=new Thread(){
18
public void run() {
19
for(int j=25;j<50;j++){
20
firstHashMap.put(String.valueOf(j), String.valueOf(j));
21
}
22
}
23
};
24
25
t1.start();
26
t2.start();
27
28
//主線程休眠1秒鐘,以便t1和t2兩個線程將firstHashMap填裝完畢。
29
Thread.currentThread().sleep(1000);
30
31
for(int l=0;l<50;l++){
32
//如果key和value不同,說明在兩個線程put的過程中出現異常。
33
if(!String.valueOf(l).equals(firstHashMap.get(String.valueOf(l)))){
34
System.err.println(String.valueOf(l)+":"+firstHashMap.get(String.valueOf(l)));
35
}
36
}
37
38
}
39
40
}

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

上面的代碼在多次執行后,發現表現很不穩定,有時沒有異常文案打出,有時則有個異常出現:
為什么會出現這種情況,主要看下HashMap的實現:
1
public V put(K key, V value) {
2
if (key == null)
3
return putForNullKey(value);
4
int hash = hash(key.hashCode());
5
int i = indexFor(hash, table.length);
6
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
7
Object k;
8
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
9
V oldValue = e.value;
10
e.value = value;
11
e.recordAccess(this);
12
return oldValue;
13
}
14
}
15
16
modCount++;
17
addEntry(hash, key, value, i);
18
return null;
19
}

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

我覺得問題主要出現在方法addEntry,繼續看:
1
void addEntry(int hash, K key, V value, int bucketIndex) {
2
Entry<K,V> e = table[bucketIndex];
3
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
4
if (size++ >= threshold)
5
resize(2 * table.length);
6
}

2

3

4

5

6

從代碼中,可以看到,如果發現哈希表的大小超過閥值threshold,就會調用resize方法,擴大容量為原來的兩倍,而擴大容量的做法是新建一個Entry[]:
1
void resize(int newCapacity) {
2
Entry[] oldTable = table;
3
int oldCapacity = oldTable.length;
4
if (oldCapacity == MAXIMUM_CAPACITY) {
5
threshold = Integer.MAX_VALUE;
6
return;
7
}
8
9
Entry[] newTable = new Entry[newCapacity];
10
transfer(newTable);
11
table = newTable;
12
threshold = (int)(newCapacity * loadFactor);
13
}

2

3

4

5

6

7

8

9

10

11

12

13

一般我們聲明HashMap時,使用的都是默認的構造方法:HashMap<K,V>,看了代碼你會發現,它還有其它的構造方法:HashMap(int initialCapacity, float loadFactor),其中參數initialCapacity為初始容量,loadFactor為加載因子,而之前我們看到的threshold = (int)(capacity * loadFactor); 如果在默認情況下,一個HashMap的容量為16,加載因子為0.75,那么閥值就是12,所以在往HashMap中put的值到達12時,它將自動擴容兩倍,如果兩個線程同時遇到HashMap的大小達到12的倍數時,就很有可能會出現在將oldTable轉移到newTable的過程中遇到問題,從而導致最終的HashMap的值存儲異常。
JDK1.0引入了第一個關聯的集合類HashTable,它是線程安全的。HashTable的所有方法都是同步的。
JDK2.0引入了HashMap,它提供了一個不同步的基類和一個同步的包裝器synchronizedMap。synchronizedMap被稱為有條件的線程安全類。
JDK5.0util.concurrent包中引入對Map線程安全的實現ConcurrentHashMap,比起synchronizedMap,它提供了更高的靈活性。同時進行的讀和寫操作都可以并發地執行。
所以在開始的測試中,如果我們采用ConcurrentHashMap,它的表現就很穩定,所以以后如果使用Map實現本地緩存,為了提高并發時的穩定性,還是建議使用ConcurrentHashMap。
====================================================================
另外,還有一個我們經常使用的ArrayList也是非線程安全的,網上看到的有一個解釋是這樣:
一個 ArrayList 類,在添加一個元素的時候,它可能會有兩步來完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在單線程運行的情況下,如果 Size = 0,添加一個元素后,此元素在位置 0,而且 Size=1;
而如果是在多線程情況下,比如有兩個線程,線程 A 先將元素存放在位置 0。但是此時 CPU 調度線程A暫停,線程 B 得到運行的機會。線程B也將元素放在位置0,(因為size還未增長),完了之后,兩個線程都是size++,結果size變成2,而只有items[0]有元素。
util.concurrent包也提供了一個線程安全的ArrayList替代者CopyOnWriteArrayList。