勇哥注:
《多线程安全》这个系列会持续写下去,它是我的一个弱点,有兴趣的朋友可以选择性看看。
上节说到lock锁关键字,它实际上是Monitor的语法糖。
lock锁定的是一个内存地址的引用。
lock必须锁定一个引用类型的变量。
锁定的变量msdn推荐是下面这样的:
private static readonly object lockobj = new object();
这里为什么必须这样是有玄机的。我们来依次看几个例子。
(一)锁定null
null在定义的时候当引用类型,执行的时候会报错误。
这里需要注意的是,有时候这样用vs是不会报错的,因为线程会把异常吃掉,这个时候你用try catch把它括起来就可以看到异常。
这可能跟vs版本或者异常设置有关系。
(二)锁定的变量改为public
见代码:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApplication1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private static readonly object lockobj = new object(); private void button1_Click(object sender, EventArgs e) { var list1 = new List<int>(); for (int i = 0; i < 10000; i++) { Task.Run(() => { lock (lockobj) { list1.Add(i); } }); } Thread.Sleep(6000); Console.WriteLine(list1.Count); } private void button2_Click(object sender, EventArgs e) { test1.show(); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (test1.lockobj) { Console.WriteLine($"{i},{k},start...[{Thread.CurrentThread.ManagedThreadId}]"); Thread.Sleep(1000); Console.WriteLine($"{i},{k},end...[{Thread.CurrentThread.ManagedThreadId}]"); } }); } } } public class test1 { //注意这里是public public static readonly object lockobj = new object(); public static void show() { for(int i=0;i<5;i++) { int k = i; Task.Run(() => { lock(lockobj) { Console.WriteLine($"{i},{k},test1start...[{Thread.CurrentThread.ManagedThreadId}]"); Thread.Sleep(1000); Console.WriteLine($"{i},{k},test1end...[{Thread.CurrentThread.ManagedThreadId}]"); } }); } } } }
这里lock后,执行次序就变成先start,再end
这里由于lock后程序的执行变为单线程模式。先是10号线程工作,它完成之间其它线程需要排除。
如果没有lock,是乱序的。
如果lock的变量置为public, 则两个线程之间是相互阻塞的,而不是并发的。
这个就是public的潜在问题。
因此如果两个线程想并发,请各自锁自己的变量,不要锁相同的变量。
(三)锁定的变量去掉static
把上面的代码改一下:
public class test2 { //注意这里没有static public readonly object lockobj = new object(); public void show(int pos) { for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (lockobj) { Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]"); Thread.Sleep(1000); Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]"); } }); } } } private void button3_Click(object sender, EventArgs e) { var t1= new test2(); t1.show(1); var t2 = new test2(); t2.show(2); }
程序结果单次调用内部肯定是顺序执行的,即先有start, 才有end,因为有lock
问题是两次调用之间可以并发吗?
答案是可以。
因为没有static后是成员变量,实例化后两个lockobj 变量是两个对象。
如果你希望多个类的实例共用一个变量,就要加static
执行结果中箭头所示线程10和15同时start,证明两次调用间是并发关系。
(四)锁定的变量为string
由于字符串也是引用类型,能不能锁字符串呢?
答案是最好不要!!
我们把代码改下,试完你就知道了。
private void button4_Click(object sender, EventArgs e) { var t1 = new test3(); t1.show(1); var t2 = new test3(); t2.show(2); } public class test3 { //注意这里字符串,它也是引用类型 public readonly string lockobj = "勇哥"; public void show(int pos) { for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (lockobj) { Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]"); Thread.Sleep(1000); Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]"); } }); } } }
从执行结果上来看,两次调用间是不能并发的。
这个原因是C#的字符串是类似享元模式的。
虽然lockobj 是两个类的实例的成员变量,理论是不同的两个变量。lock不是锁定变量而是锁定变量的引用,但是由于享元模式,这些变量的引用是同一个对象。
就算是有1000个test3类的实例,在堆里面只装着一个“勇哥”。
有关C#字符串的享元模式的知识点,参见下面的贴子:
(四)范型类的问题
上代码:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApplication1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private static readonly object lockobj = new object(); private void button1_Click(object sender, EventArgs e) { //泛型类在类型参数相同的情况下,它是相同的类 //泛型类在类型参数不同的情况下,它是不同的类 test1<int>.show(1); test1<int>.show(2); test1<test2>.show(3); } } public class test1<T> { private static readonly object lockobj = new object(); public static void show(int index) { for(int i=0;i<5;i++) { int k = i; Task.Run(() => { lock(lockobj) { Console.WriteLine($"{i},{k},test1start[{index}]...[{Thread.CurrentThread.ManagedThreadId}]"); Thread.Sleep(1000); Console.WriteLine($"{i},{k},test1end[{index}]...[{Thread.CurrentThread.ManagedThreadId}]"); } }); } } } public class test2 { public readonly object lockobj = new object(); public void show(int pos) { for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (lockobj) { Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]"); Thread.Sleep(1000); Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]"); } }); } } } }
对于下面的调用,我们想知道的问题是: 1和3能不能并发?
test1<int>.show(1);
test1<int>.show(2);
test1<test2>.show(3);
答案是可以。
因为泛型类在类型参数类型相同的情况下,它是相同的类。
泛型类在类型参数不同的情况下,它是不同的类。
这意味着1和3的锁变量lockobj 虽然是static的,但是却有两个不同的副本。
(五)this的问题
this当前类的实例,如果锁this
则t1与t2之间可以并发吗?
答案当然是可以。
private void button2_Click(object sender, EventArgs e) { var t1 = new test3(); t1.show(1); var t2 = new test3(); t2.show(2); } public class test3 { public void show(int pos) { for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { lock (this) { Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]"); Thread.Sleep(1000); Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]"); } }); } } }
再来看个递归调用的问题
当t1.show(1)后,代码会产生锁死的问题吗?
答案是不会。
private void button2_Click(object sender, EventArgs e) { var t1 = new test3(); t1.show(1); } public class test3 { int js = 0; public void show(int pos) { for (int i = 0; i < 5; i++) { js++; int k = i; Task.Run(() => { lock (this) { Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]"); Thread.Sleep(1000); Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]"); } }); if (this.js < 5) { this.show(pos); } else { break; } } } }
---------------------
作者:hackpig
来源:www.skcircle.com
版权声明:本文为博主原创文章,转载请附上博文链接!

