`
alex09
  • 浏览: 975079 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Singleton模式与双检测锁定(DCL)

阅读更多
看OOP教材时,提到了一个双检测锁定(Double-Checked Lock, DCL)的问题,但是书上没有多介绍,只是说这是一个和底层内存机制有关的漏洞。查阅了下相关资料,对这个问题大致有了点了解。
从头开始说吧。
在多线程的情况下Singleton模式会遇到不少问题,一个简单的例子

  class Singleton {      
	private static Singleton instance = null;           
	public static Singleton instance() {      
		if (instance == null) {      
			 instance = new Singleton();      
		}      
		return instance;    
	}    
 }

 
假设这样一个场景,有两个线程调用Singleton.instance(),首先线程一判断instance是否等于null,判断完后一瞬间虚拟机把线程二调度为运行线程,线程二再次判断instance是否为null,然后创建一个Singleton实例,线程二的时间片用完后,线程一被唤醒,接下来它执行的代码依然是instance = new Singleton();
两次调用返回了不同的对象,出现问题了。

最简单的方法自然是在类被载入时就初始化这个对象:private static Singleton instance = new Singleton();

JLS(Java Language Specification)中规定了一个类只会被初始化一次,所以这样做肯定是没问题的。

但是如果要实现延迟初始化(Lazy initialization),比如这个实例初始化时的参数要在运行期才能确定,应该怎么做呢?

依然有最简单的方法:使用synchronized关键字修饰初始化方法:

  
 public synchronized static Singleton instance() {        
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

   
这里有一个性能问题:多个线程同时访问这个方法时,会因为同步而导致每次只有一个线程运行,影响程序性能。而事实上初始化完毕后只需要简单的返回instance的引用就行了。

DCL是一个“看似”有效的解决方法,先把对应代码放上来吧:

      
class Singleton {  
           private static Singleton instance = null ;  
          
           public static Singleton instance() {  
               if (instance == null ) {
                   synchronized (this) {  
                       if (instance == null)
                          instance = new Singleton();
                  }
              }
              return instance;
          }
      }


用JavaWorld上对应文章的标题来评论这种做法就是smart, but broken。来看原因:

Java 编译器为了提高程序性能会进行指令调度,CPU在执行指令时同样出于性能会乱序执行(至少现在用的大多数通用处理器都是out-of-order的),另外cache的存在也会改变数据回写内存时的顺序[2]。JMM(Java Memory Model, 见[1])指出所有的这些优化都是允许的,只要运行结果和严格按顺序执行所得的结果一样即可。

Java假设每个线程都跑在自己的处理器上,享有自己的内存,和共享的主存交互。注意即使在单核上这种模型也是有意义的,考虑到cache和寄存器会保存部分临时变量。理论上每个线程修改自己的内存后,必须立即更新对应的主存内容。但是Java设计师们认为这种约束会影响程序性能,他们试着创造了一套让程序跑得更快、但又保证线程之间的交互与预期一致的内存模型。

synchronized关键字便是其中一把利器。事实上,synchronized块的实现和Linux中的信号量 (semaphore)还是有区别的,前者过程中锁的获得和释放都会都会引发一次Memory Barrier来强制线程本地内存和主存之间的同步。通过这个机制,Java中的同步机制保证了synchronized块中指令的原子性 (atomic)。

好了,回过头来看DCL问题。看起来访问一个未同步的instance字段不会产生什么问题,我们再次来假设一个场景:

线程一进入同步块,执行instance = new Singleton(); 线程二刚开始执行getResource();

按照顺序的话,接下来应该执行的步骤是 1) 分配新的Singleton对象的内存 2) 调用Singleton的构造器,初始化成员字段 3) instance被赋为指向新的对象的引用。

前面说过,编译器或处理器都为了提高性能都有可能进行指令的乱序执行,线程一的真正执行步骤可能是1) 分配内存 2) instance指向新对象 3) 初始化新实例。如果线程二在2完成后3执行前被唤醒,它看到了一个不为null的instance,跳出方法体走了,带着一个还没初始化的 Singleton对象。

错误发生的一种情形就是这样,关于更详细的编译器指令调度导致的问题,可以参看这个网页 [4]。

[3] 中提供了一个编译器指令调度的证据

instance = new Singleton(); 这条命令在Symantec JIT中被编译成

0206106A   mov         eax,0F97E78h
0206106F   call        01F6B210                  ; 分配空间
02061074   mov         dword ptr [ebp],eax       ; EBP中保存了instance的地址

02061077   mov         ecx,dword ptr [eax]       ; 解引用,获得新的指针地址

02061079   mov         dword ptr [ecx],100h      ; 接下来四行是inline后的构造器
0206107F   mov         dword ptr [ecx+4],200h   
02061086   mov         dword ptr [ecx+8],400h
0206108D   mov         dword ptr [ecx+0Ch],0F84030h

可以看到,赋值完成在初始化之前,而这是JLS允许的。

另一种情形是,假设线程一安稳地完成Singleton对象的初始化,退出了同步块,并同步了和本地内存和主存。线程二来了,看到一个非空的引用,拿走。注意线程二没有执行一个Read Barrier,因为它根本就没进后面的同步块。所以很有可能此时它看到的数据是陈旧的。

还有很多人根据已知的几种提出了一个又一个fix的方法,但最终还是出现了更多的问题。可以参阅[3]中的介绍。

[5]中还说明了即使把instance字段声明为volatile还是无法避免错误的原因。

由此可见,安全的Singleton的构造一般只有两种方法,一是在类载入时就创建该实例,二是使用性能较差的synchronized方法。

参考资料:

[1] Java Language Specification, Second Edition, 第17章介绍了Java中线程和内存交互关系的具体细节。
[2] out-of-order与cache的介绍可以参阅Computer System, A Programmer''s Perspective的第四、五章。
[3] The "Double-Checked Locking is Broken" Declaration, http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
[4] Synchronization and the Java Memory Model, http://gee.cs.oswego.edu/dl/cpj/jmm.html
[5] Double-checked locking: Clever, but broken, http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html?page=1
[6] Holub on Patterns, Learning Design Patterns by Looking at Code
分享到:
评论

相关推荐

    C++完美实现Singleton模式

    ### C++中实现Singleton模式的关键知识点 #### 一、Singleton模式简介 Singleton模式是一种常用的软件设计模式,旨在确保一个类只有一个实例,并提供一个全局访问点。这种模式在系统中经常被用于控制对共享资源...

    C++ 实现的singleton 模式

    **C++实现的Singleton模式详解** Singleton模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。这种模式在很多场景下都非常有用,例如管理共享资源,如数据库连接池,或者确保某个...

    Java常用设计模式(SingleTon、FactoryMethod、AbstractFactory)

    这里我们将深入探讨三种常见的Java设计模式:单例(Singleton)、工厂方法(Factory Method)和抽象工厂(Abstract Factory)。 **单例模式(Singleton)** 单例模式确保一个类只有一个实例,并提供一个全局访问点...

    (创建型模式)Singleton模式

    3. 双重检查锁定(DCL,Double Check Locking): 这种方式在保证线程安全的同时,也延迟了Singleton实例的创建。这种方式在多线程环境下是安全的,同时也避免了不必要的实例化。 ```java public class Singleton ...

    单例模式(饿汉模式、懒汉模式、DCL单例模式、枚举)

    本文将详细讨论四种常见的单例实现方式:饿汉模式、懒汉模式、双重检查锁定(DCL)单例模式以及枚举单例。 1. **饿汉模式**: 饿汉模式是在类加载时就完成了实例化,避免了线程同步问题。这种方式简单且安全,但...

    Java的Singleton模式代码(免资源分)

    ### Java的Singleton模式详解 #### 一、Singleton模式概述 Singleton模式是一种常用的设计模式,在Java中主要用于确保一个类只有一个实例,并提供一个全局访问点。这种模式对于管理共享资源(如数据库连接池、...

    C#设计模式之Singleton模式

    《C#设计模式之Singleton模式详解》 Singleton模式是软件设计模式中的一种基础模式,它在众多设计模式中占有重要地位,尤其在C#编程中经常被应用。Singleton模式的主要目的是确保一个类只有一个实例,并提供一个...

    最简单的设计模式学习Singleton模式

    ### 最简单的设计模式学习:Singleton模式 #### 一、Singleton模式简介 Singleton(单例)模式是一种常用的软件设计模式,其主要目的是确保一个类只有一个实例,并提供一个全局访问点。这种模式在许多场合下非常...

    Java线程安全的Singleton模式:深入分析与实现

    通过使用饿汉式、懒汉式与双重检查锁定、静态内部类或枚举等方式,可以确保Singleton实例的唯一性和线程安全。每种方式都有其适用场景和优缺点,开发者应根据实际需求选择合适的实现方式。正确实现Singleton模式,...

    设计模式 创建型模式 Singleton模式(单键)

    Singleton模式: 确保一个类只有唯一的一个实例。 Singleton主要用于对象的创建,这意味着,如果某个类采用了Singleton模式,则在这个类被创建后,它将有且仅有一个实例可供访问。很多时候我们都会需要Singleton...

    Singleton模式源程序

    Singleton模式是一种设计模式,它是创建型模式的一种,用于控制类的实例化过程,确保一个类在整个应用程序中只有一个实例存在。这种模式在系统中需要频繁创建和销毁对象,且对象需要共享资源时非常有用,比如配置...

    java Singleton单例模式

    Java中的Singleton(单例模式)是一种常用的软件设计模式,它保证了类只有一个实例,并提供一个全局访问点。这种模式在需要频繁创建和销毁对象的场景中特别有用,因为它可以节省系统资源,例如数据库连接或者线程池...

    Qt qml Singleton 单例模式

    在Qml中,我们可以通过Qt的Singleton组件来实现这一模式。 首先,让我们理解单例模式的基本概念。在软件工程中,单例模式保证一个类只有一个实例,并提供一个全局访问点。这个设计模式在许多场景下都很实用,比如...

    设计模式-Singleton与Factory

    设计模式-Singleton与Factory

    singleton设计模式java实现及对比

    **Singleton设计模式** Singleton设计模式是软件工程中最常用的设计模式之一,它的主要目的是确保一个类只有一个实例,并提供全局访问点。在Java中,Singleton模式的实现有多种方式,每种方式都有其优缺点,我们将...

    C#面向对象设计模式纵横谈-1.Singleton 单件(创建型模式)

    Singleton模式的实现有多种变体,例如懒汉式(Lazy Initialization)、饿汉式(Eager Initialization)和双检锁/双重检查锁定(Double-Check Locking)。懒汉式是在第一次使用时才创建实例,而饿汉式则是在类加载时...

    Singleton模式

    Singleton模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。这个模式在许多场景下非常有用,比如配置管理、日志服务、线程池等,需要确保全系统内只有一个对象来处理特定任务的情况...

    深入浅出单例Singleton模式

    【深入浅出单例Singleton模式】 单例模式是一种在软件设计中常见的设计模式,它的核心目标是确保一个类只有一个实例,并提供一个全局访问点。在Java等面向对象编程语言中,单例模式常用于控制资源的共享,如全局...

    设计模式之Singleton

    3. **双重检查锁定(DCL)**:结合了懒汉式和饿汉式的优点,既实现了延迟初始化,又保证了线程安全。 ```java public class Singleton { private volatile static Singleton instance; private Singleton() {...

Global site tag (gtag.js) - Google Analytics