在多線程內使用集合,如果未對集合做任何安全處理,就非常容易出現系統崩潰或各種錯誤。最近的項目裡,使用的是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,又是如何出現數據不一致的