在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前面两篇文章,分别简述了多线程的使用和发展历程,但是使用多线程无法避免的一个问题就是多线程安全。那什么是多线程安全?如何解决多线程安全?本文主要通过一些简单的小例子,简述多线程相关的问题,仅供学习分享使用,如有不足之处,还请指正。 什么是多线程安全?一段程序,单线程和多线程执行结果不一致,就表示存在多线程安全问题,即多线程不安全。 多线程安全示例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 } 然后运行示例,如下所示:
通过对以上示例进行分析,得出结论如下:
为了解决上述问题,可以通过引入局部变量来解决,即每次循环声明一个变量,循环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是不成对,且不相邻出现,但是有同一代码块的内部顺序】,效果如下: 为什么锁对象要用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 } 测试示例,如下所示:
通过以上示例,得出结论如下:
加锁锁定的是什么?在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 } 运行上述示例,结果如下:
通过上述示例,得出结论如下:
泛型锁对象如果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 } 运行上述示例,如下所示:
通过分析上述示例,得出结论如下所示:
递归加锁如果在递归函数中进行加锁,会造成死锁吗?示例代码如下: 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 } 运行上述示例,如下所示:
通过运行上述示例,得出结论如下:
备注以上就是多线程安全的简单介绍,旨在抛砖引玉,大家一起学习,共同进步。 酬乐天扬州初逢席上见赠【作者】刘禹锡 弃置身。 翻似烂柯人。 病树前头万木春。 长精神。
|
请发表评论