Lock同步锁浅析
定义:lock确保当一个线程位于代码的临界区时,另一个线程不进入临界区,如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。Monitor方法是静态的,不需要生成Monitor类的实例就可以直接调用它们,在.NET Framework中,每个对象都有一个与之关联的锁,对象可以得到并释放它以便于在任意时间只有一个线程可以访问对象实例变量和方法。Lock语句就是通过Monitor.Enter()和Monitor.Exit()实现的。当Lock(lockInstance){ }结构开始执行时调用Monitor.Enter(lockInstance)锁定lockInstance临界区,当该结构执行结束,调用monitor.Exit(lockInstance)释放lockInstance临界区。
原理:对于任何一个对象来说,它在内存中的第一部分放置的是所有方法的地址,第二部分放着一个索引,这个索引指向CLR中的SyncBlock Cache区域中的一个SyncBlock,当你执行Monitor.Enter(Object)时,如果object的索引值为负数,就从SyncBlock Cache中选取一个SyncBlock将其地址放在object的索引中,这样就完成了以object为标志的锁定,其他的线程想再次进行Monitor.Enter(object)操作,将获得object的已经为正值的索引,然后就等待,直到索引变为负数,即调用Monitor.Exit(object)将索引变为负数,等待的线程开始执行。
lock语句具有以下格式:
lock (x) { // Your code... }
其中x是引用类型的表达式,lock(x)完全等同于:
object __lockObj = x; bool __lockWasTaken = false; try { System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken); //锁定临界区 // Your code... } finally { if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj); //释放临界区 }
准则:当同步对共享资源的线程访问时,请锁定专用对象实例(例如,private readonly object balanceLock = new object();
),避免对不同的共享资源使用相同的lock对象实例,因为这可能导致死锁或锁争用。 具体而言,避免将以下对象用作lock对象:
1、避免对不同的共享资源使用相同的lock对象实例,防止死锁:
class locktest { private readonly object commonLock = new object(); //假设ShareResourcesA,ShareResourcesA这两个共享资源使用相同的锁commonLock lock(commonLock) { //这里先同步ShareResourcesA资源,commonLock对象被锁定 //do something... lock(commonLock) //因为commonLock对象被锁定,所以这里等待释放,永久等待造成死锁 { //这里先同步ShareResourcesB资源,commonLock对象被锁定 //do something... } //同步ShareResourcesB资源完成,commonLock对象被释放 } //同步ShareResourcesA资源完成,commonLock对象被释放 }
2、避免使用lock(this),因为调用方可能将其用作lock导致死锁,另外如果外部需要调用此对象则会发生阻塞不稳定的现象:
using System; using System.Threading; namespace Namespace1 { class C1 { private bool deadlocked = true; //这个方法用到了lock,我们希望lock的代码在同一时刻只能由一个线程访问 public void LockMe(object o) { lock (this) { while(deadlocked) { deadlocked = (bool)o; Console.WriteLine("Foo: I am locked :("); Thread.Sleep(500); } } } //所有线程都可以同时访问的方法 public void DoNotLockMe() { Console.WriteLine("I am not locked :)"); } } class Program { static void Main(string[] args) { C1 c1 = new C1(); //在主线程中lock c1 lock(c1) { //调用没有被lock的方法 c1.DoNotLockMe(); //调用被lock的方法,并试图将deadlock解除,将出现死锁 c1.LockMe(false); } } } }
3、避免使用lock("string"),因为字符串被公共语言运行库“暂留(intern pool)”。当有多个字符串变量包含了同样的字符串实际值时,CLR可能不会为它们重复地分配内存,而是让它们统统指向同一个字符串对象实例。所以一旦将这个字符串实例锁住了,那么整个应用程序中的所有定义了相同的字符串值的实例都不能正常的执行了。
String s1 = "Hello"; String s2 = "Hello"; bool same = (object)s1 == (object)s2; //返回true
4、避免使用lock(typeof(Class)),因为和lock("string")一样范围太大了,可能会导致需要正常运行的实例不可用。
5、lock必须锁定引用类型且不为null,因为如果传入值类型会装箱,下次代码运行到这里又会装箱,这样每次lock的都将是一个新的不同的对象,所以锁不住。
6、让我们人为用嵌套lock制造一个死锁:
A a= new A(); B b= new B();
lock(a) { //do.... lock (b) { //do...... } }
lock(b) { //do.... lock (a) { //do...... } }
假设同时执行代码2和代码3将导致死锁。
7、建议将lock锁的对象设置成private static readonly object obj = new object();如果只针对当前对象lock锁可以去掉static,静态的锁对象可以被多个实例共用,readonly是确保对象不被修改以防止锁失败,应避免锁定public类型,否则实例将超出代码的控制范围,防止外部类也锁定这个obj对象,设置成private则外部类无权访问。