前面兩篇文章,分別簡述了多執行緒的使用和發展歷程,但是使用多執行緒無法避免的一個問題就是多執行緒安全,那什么是多執行緒安全?如何解決多執行緒安全?本文主要通過一些簡單的小例子,簡述多執行緒相關的問題,僅供學習分享使用,如有不足之處,還請指正,
什么是多執行緒安全?
一段程式,單執行緒和多執行緒執行結果不一致,就表示存在多執行緒安全問題,即多執行緒不安全,
多執行緒安全示例
1. 多執行緒不安全示例1
假如我們有一個需求,需要輸出5個執行緒,且執行緒式號按0-4命名,我們撰寫代碼如下:
1 private void btnTask1_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine("【開始】**************執行緒不安全示例btnTask1_Click**************"); 4 5 for (int i = 0; i < 5; i++) 6 { 7 Task.Run(() => 8 { 9 Console.WriteLine($"【BEGIN】**************這是第 {i} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 10 Thread.Sleep(2000); 11 Console.WriteLine($"【 END 】**************這是第 {i} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 12 }); 13 } 14 15 Console.WriteLine("【結束】**************執行緒不安全示例btnTask1_Click**************"); 16 }
然后運行示例,如下所示:
通過對以上示例進行分析,得出結論如下:
- 在for回圈中,啟動的5個執行緒,執行緒式號都是5,并沒有按照我們預期的結果【0,1,2,3,4】進行輸出,
- 經過分析發現,因為for回圈中,i是同一個變數,執行緒啟動是異步進行的,存在延遲,當執行緒啟動時,for回圈已經結束,i的值為5,所以才導致執行緒式號和預期不一致,
為了解決上述問題,可以通過引入區域變數來解決,即每次回圈宣告一個變數,回圈5次,存在5個變數,則相互之間不會覆寫,如下所示:
1 private void btnTask1_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine("【開始】**************執行緒不安全示例btnTask1_Click**************"); 4 5 for (int i = 0; i < 5; i++) 6 { 7 int k = i; 8 Task.Run(() => 9 { 10 Console.WriteLine($"【BEGIN】**************這是第 {k} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 11 Thread.Sleep(2000); 12 Console.WriteLine($"【 END 】**************這是第 {k} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 13 }); 14 } 15 16 Console.WriteLine("【結束】**************執行緒不安全示例btnTask1_Click**************"); 17 }
運行優化后的示例,如下所示:

通過運行示例發現,區域變數可以解決相應的問題,
2. 多執行緒不安全示例2
假如我們有一個需求:將0到200增加到一個串列中,采用多執行緒來實作,如下所示:
1 private void btnTask2_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine("【開始】**************執行緒不安全示例btnTask1_Click**************"); 4 List<int> list = new List<int>(); 5 List<Task> tasks = new List<Task>(); 6 for (int i = 0; i < 200; i++) 7 { 8 tasks.Add( Task.Run(() => 9 { 10 list.Add(i); 11 })); 12 } 13 Task.WaitAll(tasks.ToArray()); 14 string res = string.Join(",", list); 15 Console.WriteLine($"串列長度: {list.Count} ,串列內容:{res}"); 16 Console.WriteLine("【結束】**************執行緒不安全示例btnTask1_Click**************"); 17 }
通過運行示例,如下所示:

通過對以上示例進行分析,得出結論如下:
- 串列的記錄條數不對,會少,
- 串列的元素內容與預期的內容不一致,
針對上述問題,采用中間區域變數的方式,可以解決嗎?不妨一試,修改后的 代碼如下:
1 private void btnTask2_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine("【開始】**************執行緒不安全示例btnTask1_Click**************"); 4 List<int> list = new List<int>(); 5 List<Task> tasks = new List<Task>(); 6 for (int i = 0; i < 200; i++) 7 { 8 int k = i; 9 tasks.Add( Task.Run(() => 10 { 11 list.Add(k); 12 })); 13 } 14 Task.WaitAll(tasks.ToArray()); 15 string res = string.Join(",", list); 16 Console.WriteLine($"串列長度: {list.Count} ,串列內容:{res}"); 17 Console.WriteLine("【結束】**************執行緒不安全示例btnTask1_Click**************"); 18 }
運行優化示例,如下所示:

通過運行上述示例,得出結論如下:
- 串列長度依然不對,會小于實際單一執行緒的長度,注意:多執行緒串列長度不是一定會小于單一執行緒運行時串列長度,只是存在概率,即多個執行緒存在同時寫入一個位置的概率,
- 串列內容,采用區域變數,可以解區域分問題,
由此可以得出List不是執行緒安全的資料型別,
加鎖lock
針對多執行緒的不安全問題,可以通過加鎖進行解決,加鎖的目的:在任意時刻,加鎖塊都之允許一個執行緒訪問,
加鎖原理
lock實際是一個語法糖,實際效果等同于Monitor,鎖定的是參考物件的一個記憶體地址參考,所以鎖定物件不可以是值型別,也不可以是null,只能是參考型別,
lock物件的標準寫法:默認情況下,鎖物件是私有,靜態,只讀,參考物件,如下所示:
1 /// <summary> 2 /// 定義一個鎖物件 3 /// </summary> 4 private static readonly object obj = new object();
然后優化程式,如下所示:
1 private void btnTask2_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine("【開始】**************執行緒不安全示例btnTask1_Click**************"); 4 List<int> list = new List<int>(); 5 List<Task> tasks = new List<Task>(); 6 for (int i = 0; i < 200; i++) 7 { 8 int k = i; 9 tasks.Add( Task.Run(() => 10 { 11 lock (obj) 12 { 13 list.Add(k); 14 } 15 })); 16 } 17 Task.WaitAll(tasks.ToArray()); 18 string res = string.Join(",", list); 19 Console.WriteLine($"串列長度: {list.Count} ,串列內容:{res}"); 20 Console.WriteLine("【結束】**************執行緒不安全示例btnTask1_Click**************"); 21 }
運行優化后的示例,如下所示:

通過對上述示例進行分析,得出結論如下:
- 加鎖后,串列在多執行緒下也變成安全,符合預期的要求,
- 但是由于加鎖的原因,同一時刻,只能由一個執行緒進入,其他執行緒就會等待,所以多執行緒也變成了單執行緒,
為何鎖物件要用私有型別?
標準寫法,鎖物件是私有型別,目的是為了避免鎖物件被其他執行緒使用,如果被使用,則會相互阻塞,如下所示:
假如,現在有一個鎖物件,在TestLock中使用,如下所示:
1 public class TestLock 2 { 3 public static readonly object Obj = new object(); 4 5 public void Show() 6 { 7 8 Console.WriteLine("【開始】**************執行緒示例Show**************"); 9 10 for (int i = 0; i < 5; i++) 11 { 12 int k = i; 13 Task.Run(() => 14 { 15 lock (Obj) 16 { 17 Console.WriteLine($"【BEGIN】*********T*****這是第 {k} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 18 Thread.Sleep(2000); 19 Console.WriteLine($"【 END 】*********T*****這是第 {k} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 20 } 21 }); 22 } 23 24 Console.WriteLine("【結束】**************執行緒示例Show**************"); 25 } 26 }
同時在FrmMain中使用,如下所示:
1 private void btnTask3_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine("【開始】**************執行緒示例btnTask3_Click**************"); 4 //類物件中多執行緒 5 TestLock.Show(); 6 //主方法中多執行緒 7 for (int i = 0; i < 5; i++) 8 { 9 int k = i; 10 Task.Run(() => 11 { 12 lock (TestLock.Obj) 13 { 14 Console.WriteLine($"【BEGIN】*********M*****這是第 {k} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 15 Thread.Sleep(2000); 16 Console.WriteLine($"【 END 】*********M*****這是第 {k} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 17 } 18 }); 19 } 20 21 Console.WriteLine("【結束】**************執行緒示例btnTask3_Click**************"); 22 }
運行上述示例,如下所示:

通過上述示例,得出結論如下:
- T和M是成對相鄰,且各代碼塊互動出現,
- 多個代碼塊,共用一把鎖,是會相互阻塞的,這也是為啥不建議使用public修飾符的原因,避免被不恰當的加鎖,
如果使用不同的鎖物件,多個代碼塊之間是可以并發的【T和M是不成對,且不相鄰出現,但是有同一代碼塊的內部順序】,效果如下:

為什么鎖物件要用static型別?
假如物件不是static型別,那么鎖物件就是物件屬性,不同的物件之間是相互獨立的,所以不同通物件呼叫相同的方法,就會存在并發的問題,如下所示:
修改TestLock代碼【去掉static】,如下所示:
1 public class TestLock 2 { 3 public readonly object Obj = new object(); 4 5 public void Show(string name) 6 { 7 8 Console.WriteLine("【開始】**************執行緒示例Show--{0}**************",name); 9 10 for (int i = 0; i < 5; i++) 11 { 12 int k = i; 13 Task.Run(() => 14 { 15 lock (Obj) 16 { 17 Console.WriteLine($"【BEGIN】*********T*****這是第 {k}--{name} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 18 Thread.Sleep(2000); 19 Console.WriteLine($"【 END 】*********T*****這是第 {k}--{name} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 20 } 21 }); 22 } 23 24 Console.WriteLine("【結束】**************執行緒示例Show--{0}**************",name); 25 } 26 }
宣告兩個物件,分別呼叫Show方法,如下所示:
1 private void btnTask4_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine("【開始】**************執行緒示例btnTask3_Click**************"); 4 TestLock testLock1 = new TestLock(); 5 testLock1.Show("first"); 6 7 TestLock testLock2 = new TestLock(); 8 testLock2.Show("second"); 9 Console.WriteLine("【結束】**************執行緒示例btnTask3_Click**************"); 10 }
測驗示例,如下所示:

通過以上示例,得出結論如下:
- 非靜態鎖物件,只在當前物件內部進行允許同一時刻只有一個執行緒進入,但是多個物件之間,是相互并發,相互獨立的,所以建議鎖物件為static物件,
加鎖鎖定的是什么?
在lock模式下,鎖定的是記憶體參考地址,而不是鎖定的物件的值,假如將Form的鎖物件的型別改為字串,如下所示:
1 /// <summary> 2 /// 定義一個鎖物件 3 /// </summary> 4 private static readonly string obj = "花無缺";
同時TestLock類的鎖物件也改為字串,如下所示:
1 public class TestLock 2 { 3 private static readonly string obj = "花無缺"; 4 5 public static void Show(string name) 6 { 7 8 Console.WriteLine("【開始】**************執行緒示例Show--{0}**************",name); 9 10 for (int i = 0; i < 5; i++) 11 { 12 int k = i; 13 Task.Run(() => 14 { 15 lock (obj) 16 { 17 Console.WriteLine($"【BEGIN】*********T*****這是第 {k}--{name} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 18 Thread.Sleep(2000); 19 Console.WriteLine($"【 END 】*********T*****這是第 {k}--{name} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 20 } 21 }); 22 } 23 24 Console.WriteLine("【結束】**************執行緒示例Show--{0}**************",name); 25 } 26 }
運行上述示例,結果如下:

通過上述示例,得出結論如下:
- 字串是一種特殊的鎖型別,如果字串的值一致,則認為是同一個鎖物件,不同物件之間會進行阻塞,因為string型別是享元的,在記憶體堆里面只有一個花無缺,
- 如果是其他型別,則是不同的鎖物件,是可以相互并發的,
- 說明鎖定的是記憶體參考地址,而非鎖定物件的值,
泛型鎖物件
如果TestLock為泛型類,如下所示:
1 public class TestLock<T> 2 { 3 private static readonly object obj = new object(); 4 5 public static void Show(string name) 6 { 7 8 Console.WriteLine("【開始】**************執行緒示例Show--{0}**************",name); 9 10 for (int i = 0; i < 5; i++) 11 { 12 int k = i; 13 Task.Run(() => 14 { 15 lock (obj) 16 { 17 Console.WriteLine($"【BEGIN】*********T*****這是第 {k}--{name} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 18 Thread.Sleep(2000); 19 Console.WriteLine($"【 END 】*********T*****這是第 {k}--{name} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 20 } 21 }); 22 } 23 24 Console.WriteLine("【結束】**************執行緒示例Show--{0}**************",name); 25 } 26 }
那么在呼叫時,會相互阻塞嗎?呼叫代碼如下:
1 private void btnTask5_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine("【開始】**************執行緒示例btnTask5_Click**************"); 4 TestLock<int>.Show("AA"); 5 TestLock<string>.Show("BB"); 6 Console.WriteLine("【結束】**************執行緒示例btnTask5_Click**************"); 7 }
運行上述示例,如下所示:

通過分析上述示例,得出結論如下所示:
- 對于泛型類,不同型別引數之間是可以相互并發的,因為泛型類針對不同型別引數會編譯成不同的類,那對應的鎖物件,會變成不同的參考型別,
- 如果鎖物件為字串型別,則也是會相互阻塞的,只是因為字串是享元模式,
- 泛型T的不同,會編譯成不同的副本,
遞回加鎖
如果在遞回函式中進行加鎖,會造成死鎖嗎?示例代碼如下:
1 private void btnTask6_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine("【開始】**************執行緒示例btnTask6_Click**************"); 4 this.add(1); 5 Console.WriteLine("【結束】**************執行緒示例btnTask6_Click**************"); 6 } 7 8 private int num = 0; 9 10 private void add(int index) { 11 this.num++; 12 Task.Run(()=> { 13 lock (obj) 14 { 15 Console.WriteLine($"【BEGIN】**************這是第 {num} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 16 Thread.Sleep(2000); 17 Console.WriteLine($"【 END 】**************這是第 {num} 個執行緒,執行緒ID={Thread.CurrentThread.ManagedThreadId}**************"); 18 19 if (num < 5) 20 { 21 this.add(index); 22 } 23 } 24 }); 25 }
運行上述示例,如下所示:

通過運行上述示例,得出結論如下:
- 在遞回函式中進行加鎖,會進行阻塞等待,但是不會造成死鎖,
備注
以上就是多執行緒安全的簡單介紹,旨在拋磚引玉,大家一起學習,共同進步,
酬樂天揚州初逢席上見贈【作者】劉禹錫 【朝代】唐
巴山楚水凄涼地,二十三年棄置身,
懷舊空吟聞笛賦,到鄉翻似爛柯人,
沉舟側畔千帆過,病樹前頭萬木春,
今日聽君歌一曲,暫憑杯酒長精神,

作者:小六公子
出處:http://www.cnblogs.com/hsiang/
本文著作權歸作者和博客園共有,寫文不易,支持原創,歡迎轉載【點贊】,轉載請保留此段宣告,且在文章頁面明顯位置給出原文連接,謝謝,
關注個人公眾號,定時同步更新技術及職場文章
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/388885.html
標籤:.NET技术
