內容參考自: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
