在多線程內使用集合,如果未對集合做任何安全處理,就非常容易出現系統崩潰或各種錯誤。最近的項目裡,使用的是socket通信後再改變了某個集合,結果導致系統直接崩潰,且無任何錯誤系統彈出。
經排查,發現問題是執行某集合後,系統就會在一定時間內退出,最後發現是使用的一個字典集合出了問題。稍微思考後,就認定了是線程安全問題。因為此集合在其它幾個地方都有線程做循環讀取。
下面是我模擬的一個示例,沒有進行任何的安全處理:
1 class Program
2 {
3 static MyCollection mycoll;
4 static void Main(string[] args)
5 {
6 mycoll = new MyCollection();
7 Thread readT = new Thread(new ThreadStart(ReadMethod));
8 readT.Start();
9
10 Thread addT = new Thread(new ThreadStart(AddMethod));
11 addT.Start();
12 Console.ReadLine();
13 }
14 public static void AddMethod()
15 {
16 for(int i=0;i<10;i++)
17 {
18 Thread.Sleep(500);
19 mycoll.Add("a"+i, i);
20 }
21 }
22 public static void ReadMethod()
23 {
24 while (true)
25 {
26 Thread.Sleep(100);
27 foreach (KeyValuePair<string, int> item in mycoll.myDic)
28 {
29 Console.WriteLine(item.Key + "\\t" + item.Value);
30 //其它處理
31 Thread.Sleep(2000);
32 }
33 }
34 }
35 }
36 public class MyCollection
37 {
38 public Dictionary<string, int> myDic = new Dictionary<string, int>();
39
40 public void Add(string key, int value)
41 {
42 if (myDic.ContainsKey(key))
43 {
44 myDic[key] += 1;
45 }
46 else
47 {
48 myDic.Add(key, value);
49 }
50 }
51
52 public void Remove(string key)
53 {
54 if (myDic.ContainsKey(key))
55 {
56 myDic.Remove(key);
57 }
58 }
59 }
在上面的示例中,創建了一個Dictionary字典對像,程序運行時,輸出了下面的錯誤:

程序運行時,輸出了上面的錯誤,僅僅輸出了一行結果
這次測試有明顯示的錯誤提示,集合已修改;可能無法執行枚舉操作。
唉,真是一個常見的問題,在foreach的時侯又修改集合,就一定會出現問題了,因為foreach是只讀的,在進行遍歷時不可以對集合進行任何修改。
看到這裡,我們會想到,如果使用for循環進行逆向獲取,也許可以解決此問題。
非常可惜,字典對像沒有使用索引號獲取的辦法,下面的表格轉自(http://www.cnblogs.com/yang_sy/p/3678905.html)
Type 內部結構 支持索引 內存占用 隨機插入的速度(毫秒) 順序插入的速度(毫秒) 根據鍵獲取元素的速度(毫秒) 未排序字典 Dictionary<T,V> 哈希表 否 22 30 30 20 Hashtable 哈希表 否 38 50 50 30 ListDictionary 鏈表 否 36 50000 50000 50000 OrderedDictionary 哈希表 +數組 是 59 70 70 40 排序字典 SortedDictionary<K,V> 紅黑樹 否 20 130 100 120 SortedList<K,V> 2xArray 是 20 3300 30 40 SortList 2xArray 是 27 4500 100 180從時間復雜度來講,從字典中通過鍵獲取值所耗費的時間分別如下:
這可如何是好,只能改為可排序的對像?然後使用for解決?
我突然想到,是否可以在循環時縮短foreach,來解決此問題呢?
想到可以在循環時先copy一份副本,然後再進行循環操作,編寫代碼,查找copy的方法。真是無奈,沒有提供任何的copy方法。唉!看來人都是用來被逼的,先改個對象吧:
把Dictionary修改成了Hashtable對像(也沒有索引排序)。代碼如下:
1 class Program
2 {
3 static MyCollection mycoll;
4 static void Main(string[] args)
5 {
6 mycoll = new MyCollection();
7 Thread readT = new Thread(new ThreadStart(ReadMethod));
8 readT.Start();
9
10 Thread addT = new Thread(new ThreadStart(AddMethod));
11 addT.Start();
12 Console.ReadLine();
13 }
14 public static void AddMethod()
15 {
16 for(int i=0;i<10;i++)
17 {
18 Thread.Sleep(500);
19 mycoll.Add("a"+i, i);
20 }
21 }
22 public static void ReadMethod()
23 {
24 while (true)
25 {
26 Thread.Sleep(100);
27 foreach (DictionaryEntry item in mycoll.myDic)
28 {
29 Console.WriteLine(item.Key + " " + item.Value);
30 //其它處理
31 Thread.Sleep(2000);
32 }
33 }
34 }
35 }
36 public class MyCollection
37 {
38 public Hashtable myDic = new Hashtable();
39
40 public void Add(string key, int value)
41 {
42 if (myDic.ContainsKey(key))
43 {
44
45 myDic[key] =Convert.ToInt32(myDic[key])+ 1;
46 }
47 else
48 {
49 myDic.Add(key, value);
50 }
51 }
52
53 public void Remove(string key)
54 {
55 if (myDic.ContainsKey(key))
56 {
57 myDic.Remove(key);
58 }
59 }
60 }
代碼一如即往的報錯,錯誤信息一樣。
使用copy法試試
1 class Program
2 {
3 static MyCollection mycoll;
4 static void Main(string[] args)
5 {
6 mycoll = new MyCollection();
7 Thread readT = new Thread(new ThreadStart(ReadMethod));
8 readT.Start();
9
10 Thread addT = new Thread(new ThreadStart(AddMethod));
11 addT.Start();
12 Console.ReadLine();
13 }
14 public static void AddMethod()
15 {
16 for(int i=0;i<10;i++)
17 {
18 Thread.Sleep(500);
19 mycoll.Add("a"+i, i);
20 }
21 }
22 public static void ReadMethod()
23 {
24 Hashtable tempHt = null;
25 while (true)
26 {
27 Thread.Sleep(100);
28 tempHt = mycoll.myDic.Clone() as Hashtable;
29 Console.WriteLine("\r\n=================================\r\n");
30 foreach (DictionaryEntry item in tempHt)
31 {
32 Console.WriteLine(item.Key + " " + item.Value);
33 //其它處理
34 Thread.Sleep(2000);
35 }
36 }
37 }
38 }
39 public class MyCollection
40 {
41 public Hashtable myDic = new Hashtable();
42
43 public void Add(string key, int value)
44 {
45 if (myDic.ContainsKey(key))
46 {
47
48 myDic[key] =Convert.ToInt32(myDic[key])+ 1;
49 }
50 else
51 {
52 myDic.Add(key, value);
53 }
54 }
55
56 public void Remove(string key)
57 {
58 if (myDic.ContainsKey(key))
59 {
60 myDic.Remove(key);
61 }
62 }
63 }
輸出結果如下:

以上結果輸出
寫到這裡,我自己都有些模糊了。這文章和線程安全有毛關系。
根據msdn線程安全解釋如下:
Hashtable 是線程安全的,可由多個讀取器線程或一個寫入線程使用。多線程使用時,如果任何一個線程執行寫入(更新)操作,它都不是線程安全的。若要支持多個編寫器,如果沒有任何線程在讀取 Hashtable 對象,則對 Hashtable 的所有操作都必須通過 Synchronized 方法返回的包裝完成。
從頭到尾對一個集合進行枚舉本質上並不是一個線程安全的過程。即使一個集合已進行同步,其他線程仍可以修改該集合,這將導致枚舉數引發異常。若要在枚舉過程中保證線程安全,可以在整個枚舉過程中鎖定集合,或者捕捉由於其他線程進行的更改而引發的異常。
經過我們模擬,沒有發現多線程下錯誤,但為安全起見,我們在使用時,最好根據msdn所述,在對線程操作時加上安全鎖處理,這裡我們不需自己定義鎖對象,因為微軟直接提供了SyncRoot進行安全鎖處理。 修改後的代碼如下: 1 class Program
2 {
3 static MyCollection mycoll;
4 static void Main(string[] args)
5 {
6 mycoll = new MyCollection();
7 Thread readT = new Thread(new ThreadStart(ReadMethod));
8 readT.Start();
9
10 Thread addT = new Thread(new ThreadStart(AddMethod));
11 addT.Start();
12
13
14 Thread addT2 = new Thread(new ThreadStart(AddMethod2));
15 addT2.Start();
16
17 Thread delT = new Thread(new ThreadStart(DelMethod));
18 delT.Start();
19
20 Thread delT2 = new Thread(new ThreadStart(DelMethod2));
21 delT2.Start();
22
23 Console.ReadLine();
24 }
25
26 public static void DelMethod()
27 {
28 for (int i = 0; i < 10; i++)
29 {
30 Thread.Sleep(800);
31 if(mycoll.myDic.ContainsKey("a"+i))
32 mycoll.myDic.Remove("a" + i);
33 }
34 }
35
36 public static void DelMethod2()
37 {
38 for (int i = 0; i < 10; i++)
39 {
40 Thread.Sleep(800);
41 if (mycoll.myDic.ContainsKey("b" + i))
42 mycoll.myDic.Remove("b" + i);
43 }
44 }
45
46 public static void AddMethod2()
47 {
48 for (int i = 0; i < 10; i++)
49 {
50 Thread.Sleep(500);
51 mycoll.Add("b" + i, i);
52 }
53 }
54 public static void AddMethod()
55 {
56 for(int i=0;i<10;i++)
57 {
58 Thread.Sleep(500);
59 mycoll.Add("a"+i, i);
60 }
61 }
62 public static void ReadMethod()
63 {
64 Hashtable tempHt = null;
65 while (true)
66 {
67 Thread.Sleep(100);
68 lock (mycoll.myDic.SyncRoot)
69 {
70 tempHt = mycoll.myDic.Clone() as Hashtable;
71 }
72 Console.WriteLine("\r\n=================================\r\n");
73 foreach (DictionaryEntry item in tempHt)
74 {
75 Console.WriteLine(item.Key + " " + item.Value);
76 //其它處理
77 Thread.Sleep(600);
78 }
79 }
80 }
81 }
82 public class MyCollection
83 {
84 public Hashtable myDic = new Hashtable();
85
86 public void Add(string key, int value)
87 {
88 lock (myDic.SyncRoot)
89 {
90 if (myDic.ContainsKey(key))
91 {
92
93 myDic[key] = Convert.ToInt32(myDic[key]) + 1;
94 }
95 else
96 {
97 myDic.Add(key, value);
98 }
99 }
100 }
101
102 public void Remove(string key)
103 {
104 if (myDic.ContainsKey(key))
105 {
106 lock (myDic.SyncRoot)
107 {
108 myDic.Remove(key);
109 }
110 }
111 }
112 }

時間損耗
1 public static void ReadMethod()
2 {
3 Hashtable tempHt = null;
4 System.Diagnostics.Stopwatch stopwatch = new Stopwatch();
5 stopwatch.Start(); // 開始監視代碼運行時間
6 while (true)
7 {
8 Thread.Sleep(100);
9 lock (mycoll.myDic.SyncRoot)
10 {
11 tempHt = mycoll.myDic.Clone() as Hashtable;
12 }
13 Console.WriteLine("\r\n=================================\r\n");
14 foreach (DictionaryEntry item in tempHt)
15 {
16 Console.WriteLine(item.Key + " " + item.Value);
17 //其它處理
18 Thread.Sleep(600);
19 }
20 if (tempHt != null && tempHt.Count == 20)
21 {
22 break;
23 }
24 }
25 stopwatch.Stop(); // 停止監視
26 TimeSpan timespan = stopwatch.Elapsed; // 獲取當前實例測量得出的總時間
27 Console.WriteLine("全部加滿用時:" + timespan.Milliseconds);
28 }
29 }

好了,多線程安全問題就說到這裡,總結來說就是注意鎖在多線程中的應用。
如有此文章內存在問題,還請多多指正。
線程安全性是多線程環境下的編程必須面對的棘手的問題.本文從對集合進行迭代常常遇到的java.util.ConcurrentModificationException出發,分析了異常發生的根本原因和底層機理,給出在多線程環境下使用Java集合類的兩個正確方法,一個是將迭代器轉換為數組,另一個是使用並發集合類.掌握了這兩種方法,才能在多線程環境下正確地使用Java集合類.
理論上來說你的product list應該是線程安全的,你把你的代碼貼出來看看呗,看看你是怎麼操作product,又是如何出現數據不一致的