網(wǎng)站首頁 編程語言 正文
前面兩篇文章,分別簡述了多線程的使用和發(fā)展歷程,但是使用多線程無法避免的一個(gè)問題就是多線程安全。那什么是多線程安全?如何解決多線程安全?本文主要通過一些簡單的小例子,簡述多線程相關(guān)的問題,僅供學(xué)習(xí)分享使用,如有不足之處,還請指正。
什么是多線程安全?
一段程序,單線程和多線程執(zhí)行結(jié)果不一致,就表示存在多線程安全問題,即多線程不安全。
多線程安全示例
1. 多線程不安全示例1
假如我們有一個(gè)需求,需要輸出5個(gè)線程,且線程序號(hào)按0-4命名,我們編寫代碼如下:
private void btnTask1_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程不安全示例btnTask1_Click**************"); for (int i = 0; i < 5; i++) { Task.Run(() => { Console.WriteLine($"【BEGIN】**************這是第 {i} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】**************這是第 {i} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); }); } Console.WriteLine("【結(jié)束】**************線程不安全示例btnTask1_Click**************"); }
然后運(yùn)行示例,如下所示:
通過對以上示例進(jìn)行分析,得出結(jié)論如下:
1.在for循環(huán)中,啟動(dòng)的5個(gè)線程,線程序號(hào)都是5,并沒有按照我們預(yù)期的結(jié)果【0,1,2,3,4】進(jìn)行輸出。
2.經(jīng)過分析發(fā)現(xiàn),因?yàn)閒or循環(huán)中,i是同一個(gè)變量,線程啟動(dòng)是異步進(jìn)行的,存在延遲,當(dāng)線程啟動(dòng)時(shí),for循環(huán)已經(jīng)結(jié)束,i的值為5,所以才導(dǎo)致線程序號(hào)和預(yù)期不一致。
為了解決上述問題,可以通過引入局部變量來解決,即每次循環(huán)聲明一個(gè)變量,循環(huán)5次,存在5個(gè)變量,則相互之間不會(huì)覆蓋。如下所示:
private void btnTask1_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程不安全示例btnTask1_Click**************"); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { Console.WriteLine($"【BEGIN】**************這是第 {k} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】**************這是第 {k} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); }); } Console.WriteLine("【結(jié)束】**************線程不安全示例btnTask1_Click**************"); }
運(yùn)行優(yōu)化后的示例,如下所示:
通過運(yùn)行示例發(fā)現(xiàn),局部變量可以解決相應(yīng)的問題。
2. 多線程不安全示例2
假如我們有一個(gè)需求:將0到200增加到一個(gè)列表中,采用多線程來實(shí)現(xiàn),如下所示:
private void btnTask2_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程不安全示例btnTask1_Click**************"); List<int> list = new List<int>(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 200; i++) { tasks.Add( Task.Run(() => { list.Add(i); })); } Task.WaitAll(tasks.ToArray()); string res = string.Join(",", list); Console.WriteLine($"列表長度: {list.Count} ,列表內(nèi)容:{res}"); Console.WriteLine("【結(jié)束】**************線程不安全示例btnTask1_Click**************"); }
通過運(yùn)行示例,如下所示:
通過對以上示例進(jìn)行分析,得出結(jié)論如下:
1.列表的記錄條數(shù)不對,會(huì)少。
2.列表的元素內(nèi)容與預(yù)期的內(nèi)容不一致。
針對上述問題,采用中間局部變量的方式,可以解決嗎?不妨一試,修改后的 代碼如下:
private void btnTask2_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程不安全示例btnTask1_Click**************"); List<int> list = new List<int>(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 200; i++) { int k = i; tasks.Add( Task.Run(() => { list.Add(k); })); } Task.WaitAll(tasks.ToArray()); string res = string.Join(",", list); Console.WriteLine($"列表長度: {list.Count} ,列表內(nèi)容:{res}"); Console.WriteLine("【結(jié)束】**************線程不安全示例btnTask1_Click**************"); }
運(yùn)行優(yōu)化示例,如下所示:
通過運(yùn)行上述示例,得出結(jié)論如下:
1.列表長度依然不對,會(huì)小于實(shí)際單一線程的長度。注意:多線程列表長度不是一定會(huì)小于單一線程運(yùn)行時(shí)列表長度,只是存在概率,即多個(gè)線程存在同時(shí)寫入一個(gè)位置的概率。
2.列表內(nèi)容,采用局部變量,可以解決部分問題。
由此可以得出List不是線程安全的數(shù)據(jù)類型。
加鎖lock
針對多線程的不安全問題,可以通過加鎖進(jìn)行解決,加鎖的目的:在任意時(shí)刻,加鎖塊都之允許一個(gè)線程訪問。
加鎖原理
lock實(shí)際是一個(gè)語法糖,實(shí)際效果等同于Monitor。鎖定的是引用對象的一個(gè)內(nèi)存地址引用。所以鎖定對象不可以是值類型,也不可以是null,只能是引用類型。
lock對象的標(biāo)準(zhǔn)寫法:默認(rèn)情況下,鎖對象是私有,靜態(tài),只讀,引用對象。如下所示:
/// <summary> /// 定義一個(gè)鎖對象 /// </summary> private static readonly object obj = new object();
然后優(yōu)化程序,如下所示:
private void btnTask2_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程不安全示例btnTask1_Click**************"); List<int> list = new List<int>(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 200; i++) { int k = i; tasks.Add( Task.Run(() => { lock (obj) { list.Add(k); } })); } Task.WaitAll(tasks.ToArray()); string res = string.Join(",", list); Console.WriteLine($"列表長度: {list.Count} ,列表內(nèi)容:{res}"); Console.WriteLine("【結(jié)束】**************線程不安全示例btnTask1_Click**************"); }
運(yùn)行優(yōu)化后的示例,如下所示:
通過對上述示例進(jìn)行分析,得出結(jié)論如下:
1.加鎖后,列表在多線程下也變成安全,符合預(yù)期的要求。
2.但是由于加鎖的原因,同一時(shí)刻,只能由一個(gè)線程進(jìn)入,其他線程就會(huì)等待,所以多線程也變成了單線程。
為何鎖對象要用私有類型?
標(biāo)準(zhǔn)寫法,鎖對象是私有類型,目的是為了避免鎖對象被其他線程使用,如果被使用,則會(huì)相互阻塞,如下所示:
假如,現(xiàn)在有一個(gè)鎖對象,在TestLock中使用,如下所示:
public class TestLock { public static readonly object Obj = new object(); public void Show() { Console.WriteLine("【開始】**************線程示例Show**************"); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (Obj) { Console.WriteLine($"【BEGIN】*********T*****這是第 {k} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】*********T*****這是第 {k} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); } }); } Console.WriteLine("【結(jié)束】**************線程示例Show**************"); } }
同時(shí)在FrmMain中使用,如下所示:
private void btnTask3_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程示例btnTask3_Click**************"); //類對象中多線程 TestLock.Show(); //主方法中多線程 for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (TestLock.Obj) { Console.WriteLine($"【BEGIN】*********M*****這是第 {k} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】*********M*****這是第 {k} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); } }); } Console.WriteLine("【結(jié)束】**************線程示例btnTask3_Click**************"); }
運(yùn)行上述示例,如下所示:
通過上述示例,得出結(jié)論如下:
1.T和M是成對相鄰,且各代碼塊交互出現(xiàn)。
2.多個(gè)代碼塊,共用一把鎖,是會(huì)相互阻塞的。這也是為啥不建議使用public修飾符的原因,避免被不恰當(dāng)?shù)募渔i。
如果使用不同的鎖對象,多個(gè)代碼塊之間是可以并發(fā)的【T和M是不成對,且不相鄰出現(xiàn),但是有同一代碼塊的內(nèi)部順序】,效果如下:
為什么鎖對象要用static類型?
假如對象不是static類型,那么鎖對象就是對象屬性,不同的對象之間是相互獨(dú)立的,所以不同通對象調(diào)用相同的方法,就會(huì)存在并發(fā)的問題,如下所示:
修改TestLock代碼【去掉static】,如下所示:
public class TestLock { public readonly object Obj = new object(); public void Show(string name) { Console.WriteLine("【開始】**************線程示例Show--{0}**************",name); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (Obj) { Console.WriteLine($"【BEGIN】*********T*****這是第 {k}--{name} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】*********T*****這是第 {k}--{name} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); } }); } Console.WriteLine("【結(jié)束】**************線程示例Show--{0}**************",name); } }
聲明兩個(gè)對象,分別調(diào)用Show方法,如下所示:
private void btnTask4_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程示例btnTask3_Click**************"); TestLock testLock1 = new TestLock(); testLock1.Show("first"); TestLock testLock2 = new TestLock(); testLock2.Show("second"); Console.WriteLine("【結(jié)束】**************線程示例btnTask3_Click**************"); }
測試示例,如下所示:
通過以上示例,得出結(jié)論如下:
非靜態(tài)鎖對象,只在當(dāng)前對象內(nèi)部進(jìn)行允許同一時(shí)刻只有一個(gè)線程進(jìn)入,但是多個(gè)對象之間,是相互并發(fā),相互獨(dú)立的。所以建議鎖對象為static對象。
加鎖鎖定的是什么?
在lock模式下,鎖定的是內(nèi)存引用地址,而不是鎖定的對象的值。假如將Form的鎖對象的類型改為字符串,如下所示:
/// <summary> /// 定義一個(gè)鎖對象 /// </summary> private static readonly string obj = "花無缺";
同時(shí)TestLock類的鎖對象也改為字符串,如下所示:
public class TestLock { private static readonly string obj = "花無缺"; public static void Show(string name) { Console.WriteLine("【開始】**************線程示例Show--{0}**************",name); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (obj) { Console.WriteLine($"【BEGIN】*********T*****這是第 {k}--{name} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】*********T*****這是第 {k}--{name} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); } }); } Console.WriteLine("【結(jié)束】**************線程示例Show--{0}**************",name); } }
運(yùn)行上述示例,結(jié)果如下:
通過上述示例,得出結(jié)論如下:
1.字符串是一種特殊的鎖類型,如果字符串的值一致,則認(rèn)為是同一個(gè)鎖對象,不同對象之間會(huì)進(jìn)行阻塞。因?yàn)閟tring類型是享元的,在內(nèi)存堆里面只有一個(gè)花無缺。
2.如果是其他類型,則是不同的鎖對象,是可以相互并發(fā)的。
3.說明鎖定的是內(nèi)存引用地址,而非鎖定對象的值。
泛型鎖對象
如果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} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); 18 Thread.Sleep(2000); 19 Console.WriteLine($"【 END 】*********T*****這是第 {k}--{name} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); 20 } 21 }); 22 } 23 24 Console.WriteLine("【結(jié)束】**************線程示例Show--{0}**************",name); 25 } 26 }
那么在調(diào)用時(shí),會(huì)相互阻塞嗎?調(diào)用代碼如下:
private void btnTask5_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程示例btnTask5_Click**************"); TestLock<int>.Show("AA"); TestLock<string>.Show("BB"); Console.WriteLine("【結(jié)束】**************線程示例btnTask5_Click**************"); }
運(yùn)行上述示例,如下所示:
通過分析上述示例,得出結(jié)論如下所示:
1.對于泛型類,不同類型參數(shù)之間是可以相互并發(fā)的,因?yàn)榉盒皖愥槍Σ煌愋蛥?shù)會(huì)編譯成不同的類,那對應(yīng)的鎖對象,會(huì)變成不同的引用類型。
2.如果鎖對象為字符串類型,則也是會(huì)相互阻塞的,只是因?yàn)樽址窍碓J健?/p>
3.泛型T的不同,會(huì)編譯成不同的副本。
遞歸加鎖
如果在遞歸函數(shù)中進(jìn)行加鎖,會(huì)造成死鎖嗎?示例代碼如下:
private void btnTask6_Click(object sender, EventArgs e) { Console.WriteLine("【開始】**************線程示例btnTask6_Click**************"); this.add(1); Console.WriteLine("【結(jié)束】**************線程示例btnTask6_Click**************"); } private int num = 0; private void add(int index) { this.num++; Task.Run(()=> { lock (obj) { Console.WriteLine($"【BEGIN】**************這是第 {num} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); Thread.Sleep(2000); Console.WriteLine($"【 END 】**************這是第 {num} 個(gè)線程,線程ID={Thread.CurrentThread.ManagedThreadId}**************"); if (num < 5) { this.add(index); } } }); }
運(yùn)行上述示例,如下所示:
通過運(yùn)行上述示例,得出結(jié)論如下:
在遞歸函數(shù)中進(jìn)行加鎖,會(huì)進(jìn)行阻塞等待,但是不會(huì)造成死鎖。?
原文鏈接:https://www.cnblogs.com/hsiang/p/15709111.html
相關(guān)推薦
- 2022-06-27 詳解Python中while無限迭代循環(huán)方法_python
- 2022-09-08 Python報(bào)錯(cuò)SyntaxError:unexpected?EOF?while?parsing的解
- 2022-05-13 C++ Poco庫的編譯和使用
- 2022-05-24 Pytho的HTTP交互httpx包模塊使用詳解_python
- 2023-10-25 對于Echarts實(shí)例化與銷毀的一些運(yùn)用
- 2022-04-11 following signatures couldn‘t be verified because
- 2022-08-15 element的form表單中如何一行顯示多el-form-item標(biāo)簽
- 2022-04-14 android studio不顯示當(dāng)前手機(jī)app進(jìn)程
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支