内容参考自:http://daimajishu.iteye.com/blog/1079107
四.lock应避免锁定public 类型或不受程序控制的对象,举例
lock就是把一段代码定义为临界区,所谓临界区就是同一时刻只能有一个线程来操作临界区的代码,当一个线程位于代码的临界区时,另一个线程不能进入临界区,如果试图进入临界区,则只能一直等待(即被阻止),直到已经进入临界区的线程访问完毕,并释放锁旗标。
其基本使用方式如下:
class Test { //定义一个私有成员变量,用于Lock的锁定标志 private static object lockobj = new object(); void DoSomething() { lock (lockobj) { //需要锁定的代码块 } } }
最经典的例子,莫过于模拟银行5个窗口取钱操作的例子了,5个窗口是5个线程,都要取钱,但是同一刻只能有一个窗口可以进行真正的取钱操作(钱数的数值计算,剩余多少等这些代码必须定义为临界区),其他只有等待,其代码如下:
class Account { int balance; Random r = new Random(); public Account(int initial) { balance = initial; } int Withdraw(int amount) { // This condition will never be true unless the lock statement // is commented out: if (balance < 0) throw new Exception("Negative Balance"); // Comment out the next line to see the effect of leaving out // the lock keyword: lock (this) { if (balance >= amount) { Console.WriteLine("提款窗口: " + Thread.CurrentThread.Name); Console.WriteLine("提款之前余额(Balance before Withdrawal): " + balance); Console.WriteLine("提款数量(Amount to Withdraw) : -" + amount); balance = balance - amount; Console.WriteLine("提款之后余额(Balance after Withdrawal) : " + balance); Console.WriteLine(); return amount; } else return 0; // transaction rejected } } public void DoTransactions() { //模拟100个人来提款,每次提1-30元 for (int i = 0; i < 100; i++) Withdraw(r.Next(1, 30)); } } class Test { public static void MainXXX() { Thread[] threads = new Thread[5]; //总额为100元 Account acc = new Account(100); //定义并初始化5个线程,模拟银行的5个窗口 for (int i = 0; i < 5; i++) { Thread t = new Thread(new ThreadStart(acc.DoTransactions)) { Name = i + "号" }; threads[i] = t; } //启动5个线程,模拟银行的5个窗口开始工作 for (int i = 0; i < 5; i++) { Console.WriteLine("threads[{0}].Start()", i); threads[i].Start(); } } }
运算结果:
threads[0].Start()
threads[1].Start()
threads[2].Start()
提款窗口: 0号
提款之前余额(Balance before Withdrawal): 100
threads[3].Start()
提款数量(Amount to Withdraw) : -18
提款之后余额(Balance after Withdrawal) : 82
提款窗口: 1号
提款之前余额(Balance before Withdrawal): 82
提款数量(Amount to Withdraw) : -9
提款之后余额(Balance after Withdrawal) : 73
提款窗口: 1号
提款之前余额(Balance before Withdrawal): 73
提款数量(Amount to Withdraw) : -4
提款之后余额(Balance after Withdrawal) : 69
提款窗口: 1号
提款之前余额(Balance before Withdrawal): 69
提款数量(Amount to Withdraw) : -4
提款之后余额(Balance after Withdrawal) : 65
提款窗口: 0号
threads[4].Start()
提款之前余额(Balance before Withdrawal): 65
提款数量(Amount to Withdraw) : -12
提款之后余额(Balance after Withdrawal) : 53
提款窗口: 2号
提款之前余额(Balance before Withdrawal): 53
提款数量(Amount to Withdraw) : -26
提款之后余额(Balance after Withdrawal) : 27
提款窗口: 3号
提款之前余额(Balance before Withdrawal): 27
提款数量(Amount to Withdraw) : -27
提款之后余额(Balance after Withdrawal) : 0
1. lock不能锁定空值
某一对象可以指向Null,但Null是不需要被释放的。(请参考:认识全面的null)
2. lock不能锁定string类型,虽然它也是引用类型的。因为字符串类型被CLR“暂留”
这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。因此,最好锁定不会被暂留的私有或受保护成员。
3. lock锁定的对象是一个程序块的内存边界
4. 值类型不能被lock,因为前文标红字的“对象被释放”,值类型不是引用类型的
5. lock就避免锁定public 类型或不受程序控制的对象。
例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。
使用lock(this)的时候,类的成员变量的值可能会被不在临界区的方法改值了
class ThreadTest { private int i = 0; public void Test() { Thread t1 = new Thread(Thread1); Thread t2 = new Thread(Thread2); t1.Start(); t2.Start(); } public void Thread1() { lock (this) { Console.WriteLine(this.i); Thread.Sleep(1000); Console.WriteLine(this.i); } } public void Thread2() { Thread.Sleep(500); this.i = 1; Console.WriteLine("Change the value in locking"); } } public class ThreadTest2 { private int i = 0; public void Test() { Thread t1 = new Thread(Thread1); Thread t2 = new Thread(Thread2); t1.Start(); t2.Start(); } public void Thread1() { lock (this) { Console.WriteLine(this.i); Thread.Sleep(1000); Console.WriteLine(this.i); } } public void Thread2() { lock (this) { Thread.Sleep(500); this.i = 1; Console.WriteLine("Can't change the value in locking"); } } } public class ThreadMain { public static void Main() { //ThreadTest b = new ThreadTest(); //Thread t = new Thread(new ThreadStart(b.Test)); //t.Start(); ThreadTest2 b2 = new ThreadTest2(); Thread t2 = new Thread(new ThreadStart(b2.Test)); t2.Start(); } }
测试ThreadTest的运行结果:
0
Change the value in locking
1
测试ThreadTest2的运行结果:
0
0
Can't change the value in locking
发现第一个测试里成员变量i被改值了。
本想在案例一中lock住this对象,让其他的线程不能操作,可是事情不是像我们想象的那样lock(this)是lock this的意思.this中的属性依然能够被别的线程改变.那我们lock住的是什么?是代码段,是lock后面大括号中代码段,这段代码让多个人执行不不被允许的.那返回头来在看lock(this),this是什么意思呢?可以说this知识这段代码域的标志,看看案例二中Thread2.Thread2就明白了,Thread2中的lock需要等到Thread1种lock释放后才开始运行,释放之前一直处于等待状态,这就是标志的表现.
好吧,让我们来了解一下,lock这段代码是怎么运行的.lock语句根本使用的就是Monitor.Enter和Monitor.Exit,也就是说lock(this)时执行Monitor.Enter(this),大括号结束时执行Monitor.Exit(this).他的意义在于什么呢,对于任何一个对象来说,他在内存中的第一部分放置的是所有方法的地址,第二部分放着一个索引,他指向CLR中的SyncBlock Cache区域中的一个SyncBlock.什么意思呢?就是说,当你执行Monitor.Enter(Object)时,如果object的索引值为负数,就从SyncBlock Cache中选区一个SyncBlock,将其地址放在object的索引中。这样就完成了以object为标志的锁定,其他的线程想再次进行Monitor.Enter(object)操作,将获得object为正数的索引,然后就等待。直到索引变为负数,即线程使用Monitor.Exit(object)将索引变为负数。
如果明白了Monitor.Enter的原理,lock当然不再话下.当然lock后括号里面的值不是说把整个对象锁住,而是对他的一个值进行了修改,使别的lock不能锁住他,这才是lock(object)的真面目.
但在实际使用中Monitor还是不推荐,还是lock好的,Monitor需要加上很多try catch才能保证安全性,但lock却帮我们做了,而且lock看起来更优雅.
在静态方法中如何使用lock呢,由于我们没有this可用,所以我们使用typeof(this)好了,Type也有相应的方法地址和索引,所以他也是可以来当作lock的标志的.
但微软不提倡是用public的object或者typeof()或者字符串这样的标志就是因为,如果你的public object在其他的线程中被null并被垃圾收集了,将发生不可预期的错误.
15.10.24补充
关于锁的理解不够透彻,最近又做了一轮测试。测试结果表明:
同一个锁,只要当前线程已经拥有此锁,不管是在锁对应的哪一个代码段,都可以畅通无阻。
测试代码一:
public class LockTest { int idx = 0; public void ChangeIdx() { lock (this) { Console.WriteLine(idx); if (idx < 5) { idx++; ChangeIdx(); } } } }
测试代码二:
public class ThreadTest2 { private int i = 0; public void Test() { Thread1(); } public void Thread1() { lock (this) { Console.WriteLine(this.i); Thread.Sleep(1000); Console.WriteLine(this.i); Thread2(); } } public void Thread2() { lock (this) { Thread.Sleep(500); this.i = 1; Console.WriteLine("Can't change the value in locking"); } } }
代码一的目的是测试同代码段多次Enter的情况;代码二的目的则是不同代码段,段一还未Exit前,段二Enter。结果,两个都执行的很流畅。
MSDN参考文档:https://msdn.microsoft.com/zh-cn/library/c5kehkcz.aspx