Visual Basic.NET实现双检锁(DCL)模式(五) |
代码清单14、二重检查的线程安全的Singleton类 本文介绍了称为双检锁(Double-Check Locking简称DCL)模式的代码模式,它的工作原理及其在Singleton(单例)模式及Multiton(多例)模式中的应用,并且讨论了DCL模式在Visual Basic.net和c#语言中的实现。其中Visual Basic.NET的源代码可以在文中看到,c#的源代码在附录中给出。 ?
|
Visual Basic.NET实现双检锁(DCL)模式(五) |
代码清单14、二重检查的线程安全的Singleton类 本文介绍了称为双检锁(Double-Check Locking简称DCL)模式的代码模式,它的工作原理及其在Singleton(单例)模式及Multiton(多例)模式中的应用,并且讨论了DCL模式在Visual Basic.net和c#语言中的实现。其中Visual Basic.NET的源代码可以在文中看到,c#的源代码在附录中给出。 ?
|
Visual Basic、NET实现双检锁(DCL)模式(一) |
本文介绍了称为双检锁(Double-Check Locking简称DCL)模式的代码模式,它的工作原理及其在Singleton(单例)模式及Multiton(多例)模式中的应用,并且讨论了DCL模式在Visual Basic.net和c#语言中的实现。其中Visual Basic.NET的源代码可以在文中看到,c#的源代码在附录中给出。 本文假设读者熟悉Visual Basic.NET或C#的多线程概念、设计模式的基本概念,以及UML基本图标。 DCL模式(Double-Check Locking Pattern)有时又称作双检模式(Double-Check Pattern),只有在多线程的环境中才有用。它是从c语言移植过来的。在c语言里,DCL模式常常用在多线程环境中类的迟实例化(Late Instantiation)里。 DCL模式通常与Factory模式一同使用,用来循环使用产品对象。如果读者熟悉 Singleton(Singleton)模式的话,DCL模式可以使用到"懒汉式"的Singleton模式里面,用来提供唯一的产品对象。通过进一步推广,可以使用到Multiton模式和Flyweight模式里面。 从Factory模式谈起 为了解释什么是DCL模式,还是从Factory模式谈起吧。 在下面的类图中,工厂类Factory0有一个共享方法GetInstance()用来提供产品类Product的实例。 图1、一个由工厂类与产品类组成的系统。 Factory0的源代码如下: Public Class Factory0 Public Shared Function GetInstance() As Product Return New Product() End Function End Class 代码清单1、Factory0类的源代码 显然,只要调用GetInstance()方法就会得到Product类的实例,每一次调用得到的都是新的实例。Product类特别提供了计数的方法,通过调用GetCount()方法就可以得到Product所有实例的总数。 Public Class Product Private Shared count As Integer = 0 Public Sub New() count += 1 System.Console.WriteLine("Product number {0} is created.", count) End Sub Public Shared Function GetCount() As Integer Return count End Function End Class 代码清单2、产品类Product的源代码 但是如果产品类的实例必须循环使用,而不能无限制创建的话,工厂方法GetInstance()的内容必须改写,以实现必要的循环逻辑。而最简单的循环逻辑,就是重复使用单一的产品类实例。比如下面的源代码就实现了单一产品类实例的逻辑: Public Class Factory1 Private Shared instance As Product Public Shared Function GetInstance() As Product If (instance Is Nothing) Then instance = New Product() End If Return instance End Function End Class 代码清单3、工厂类Factory1的源代码 简单得不能再简单了吧?如果已经创建过Product类实例的话,就返还这个实例;反之,就首先创建这个实例,将之记录在案,然后再返还它。 写出这样的代码,本意显然是要保持在整个系统里只有一个Product 的实例;因此才会有 If (instance Is Nothing) Then 的检查。不很明显的是,如果在多线程的环境中运行,上面的代码会有两个甚至两个以上的Product对象被创建出来,从而造成错误。 在多线程环境里,如果有两个线程A和B几乎同时到达 If (instance Is Nothing) Then语句的外面的话,假设线程A比线程B早一点点,那么: 1. A会首先进入If (instance Is Nothing) Then 块的内部,并开始执行New Product()语句。至此时,instance变量仍然是Nothing,直到线程A的New Product()语句返回并给instance变量赋值。 2. 但是,线程B并不会在If (instance Is Nothing) Then 语句的外面等待,因为此时instance Is Nothing是成立的,它会马上进入If (instance Is Nothing) Then语句块的内部。这样,线程B会不可避免地执行instance = New Product()的语句,从而创建出第二个实例来。 3. 下面,线程A的instance = New Product()语句执行完毕,instance变量得到了真实的对象引用, (instance Is Nothing)不再为真。第三个线程不会在进入If (instance Is Nothing) Then语句块的内部了。 4. 紧接着,线程B的instance = New Product()语句也执行完毕,instance变量的值被覆盖。但是第一个Product对象被线程A引用的事实不会改变。 这时,线程A和B各自拥有一个独立的Product对象,而这是错误的。为了能够直观地看到程序执行的结果,可以运行下面的客户端代码: |
Visual Basic.NET实现双检锁(DCL)模式(四) |
问答题答案 第1题答案、Mutex改写同步化代码清单5,结果如下: Public Class Factory2A Private Shared instance As Product Private Shared m As Mutex = New Mutex() Private Sub New() System.Console.WriteLine("Factory object is created.") End Sub Public Shared Function GetInstance() As Product Thread.Sleep(10) m.WaitOne() If (instance Is Nothing) Then instance = New Product() End If m.ReleaseMutex() Return instance End Function End Class 代码清单10、二重检查的线程安全的Singleton类 第2题答案、Monitor对象提供针对一个资源对象的同步锁。使用Monitor对象改写代码清单5,结果为: Public Class Factory2B Private Shared instance As Product Private Sub New() System.Console.WriteLine("Factory object is created.") End Sub Public Shared Function GetInstance() As Product Thread.Sleep(10) Monitor.Enter(GetType(Factory2B)) If (instance Is Nothing) Then instance = New Product() End If Monitor.Exit(GetType(Factory2B)) Return instance End Function End Class 代码清单11、二重检查的线程安全的Singleton类 第3题答案、使用了SyncLock的版本如下: Public Class Factory2C Private Shared instance As Product Private Sub New() System.Console.WriteLine("Factory object is created.") End Sub Public Shared Function GetInstance() As Product Thread.Sleep(10) SyncLock (GetType(Factory2C)) If (instance Is Nothing) Then instance = New Product() End If End SyncLock Return instance End Function End Class 代码清单12、二重检查的线程安全的Singleton类 第4题答案、使用Monitor对象改写后的双检锁工厂类为: Public Class Factory3A Private Shared instance As Product Public Shared Function GetInstance() As Product Thread.Sleep(10) If (instance Is Nothing) Then Monitor.Enter(GetType(Factory3A)) If (instance Is Nothing) Then instance = New Product() End If Monitor.Exit(GetType(Factory3A)) End If Return instance End Function End Class 代码清单13、二重检查的线程安全的Singleton类 第5题答案、使用SyncLock改写后的双检锁工厂类为; Public Class Factory3B Private Shared instance As Product Public Shared Function GetInstance() As Product Thread.Sleep(10) If (instance Is Nothing) Then SyncLock (GetType(Factory3B)) If (instance Is Nothing) Then instance = New Product() End If End SyncLock End If Return instance End Function End Class |
Visual Basic.NET实现双检锁(DCL)模式(三) |
到此为止,线程A和线程B得到了同一个Product对象。可以看到,在上面的方法GetInstance ()中,同步化仅用来避免多个线程同时初始化这个类,而不是同时调用这个静态工厂方法。如果这是正确的,那么使用这一个模式之后,"懒汉式"工厂类就可以摆脱掉同步化瓶颈,达到一个完美的境界。这就是DCL模式。
|
Visual Basic、NET实现双检锁(DCL)模式(二) |
Private Sub Run1() 为了克服没有线程安全的缺点,下面给出一个线程安全的GetInstance()方法: <MethodImpl(MethodImplOptions.Synchronized)>_ Public Shared Function GetInstance() As Product Thread.Sleep(10) If (instance Is Nothing) Then instance = New Product() End If Return instance End Function 代码清单5、这是一个线程安全的正确答案 显然,由于整个静态工厂方法都是同步化的,因此,不会有两个线程同时进入这个方法。因此,当线程A和B作为第一批调用者同时或几乎同时调用此方法时: 早到一点的线程A会率先进入此方法,同时线程B会在方法外部等待; 1. 对线程A来说,instance变量的值是Nothing,因此instance = New Product()语句会被执行。 2. 线程A结束对方法的执行,instance变量的值不再是Nothing。 3. 线程B进入此方法,instance变量的值不再是Nothing,因此instance = New Product()语句不会被执行。线程B取到的是instance变量所含有的引用,也就是对线程A所创立的Product实例的引用。 显然,线程A和B持有同一个Product实例,这是正确的。 读到这里,读者可以参看本文后面的问答题1、2和3。 优化的线程安全版本---DCL模式 再进入本节的讨论之前,首先复习一下Mutex类。Mutex可以提供排他性的访问限制,通过只允许一个线程访问这个资源,从而达到同步化的目的。需要取得访问许可的线程,必须调用WaitOne()方法。如果当前没有其他线程访问,则线程可以取得访问许可;不然就会在这个语句处等待。访问结束的时候,可以调用ReleaseMutex()方法,释放访问许可。 仔细审察上面的代码清单5就会发现,同步化实际上只在instance变量第一次被赋值之前才有用。在instance变量有了值以后,同步化实际上变成了一个不必要的瓶颈。如果能有一个方法去掉这个小小的额外开销,不是更加完美了吗?因此,就有了下面这个设计巧妙的双检锁(Double-Check Locking)。 Public Class Factory3 Private Shared instance As Product Private Shared m As Mutex = New Mutex() Private Sub New() System.Console.WriteLine("Factory object is created.") End Sub Public Shared Function GetInstance() As Product Thread.Sleep(10) If (instance Is Nothing) Then '位置1 '位置2 m.WaitOne() '位置3 If (instance Is Nothing) Then '位置4 instance = New Product() End If m.ReleaseMutex() End If Return instance End Function End Class 代码清单6、使用DCL模式的懒汉式工厂类 对于初次接触DCL模式的读者来说,这个技巧的思路并不明显易懂,因此本文在这里给出一个详尽的解释。同样,这里假设线程A和B作为第一批调用者同时或几乎同时调用静态工厂方法。 1. 因为线程A和B是第一批调用者,因此当它们进入此静态工厂方法时,instance变量是Nothing。因此线程A和B会同时或几乎同时到达位置1。 2. 假设线程A会首先到达位置2,并进入m.WaitOne()并到达位置3。这时,由于m.WaitOne()的同步化限制,线程B无法到达位置3,而只能在位置2等候。 3. 线程A执行instance = New Product()语句,使得instance变量得到一个值,即对一个Product对象的引用。此时,线程B只能继续在位置2等候。 4. 线程A退出m.WaitOne(),返回instance对象,退出静态工厂方法。 5. 线程B进入m.WaitOne()块,达到位置3,进而达到位置4。由于instance变量已经不是Nothing了,因此线程B退出 m.WaitOne(),返回instance所引用的Product对象(也就是线程A所创建的Product对象),退出静态工厂方法。 |