`
crud0906
  • 浏览: 136133 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

单例模式、双检测锁定DCL、volatile(转)

阅读更多
单例模式最要关心的则是对象创建的次数以及何时被创建。

Singleton模式可以是很简单的,它的全部只需要一个类就可以完成(看看这章可怜的UML图)。但是如果在“对象创建的次数以及何时被创建”这两点上较真起来,Singleton模式可以相当的复杂,比头五种模式加起来还复杂,譬如涉及到DCL双锁检测(double checked locking)的讨论、涉及到多个类加载器(ClassLoader)协同时、涉及到跨JVM(集群、远程EJB等)时、涉及到单例对象被销毁后重建等。对于复杂的情况,本章中会涉及到其中一些[1]

目的:
希望对象只创建一个实例,并且提供一个全局的访问点。

场景:
Kerrigan对于Zerg来说是个至关重要的灵魂人物,无数的Drone、Zergling、Hydralisk……可以被创造、被牺牲,但是Kerrigan得存在关系到Zerg在这局游戏中的生存,而且Kerrigan是不允许被多次创造的,必须有且只有一个虫族刀锋女王的实例存在,这不是游戏规则,但这是个政治问题。

分析:
结构是简单的,只是我们还有一些小小的要求如下:

1.最基本要求:每次从getInstance()都能返回一个且唯一的一个Kerrigan对象。

2.稍微高一点的要求:Kerrigan很忙,很多人找,所以希望这个方法能适应多线程并发访问。

3.再提高一点的要求:Zerg是讲究公务员效率的社会,希望找Kerrigan的方法性能尽可能高。

4.最后一点要求是Kerrigan自己提出的:体谅到Kerrigan太累,希望多些睡觉时间,因此Kerrigan希望实现懒加载(Lazy Load),在需要的时候才被构造。

5.原本打算说还提要处理多ClassLoader、多JVM等情况,不过还是不要把情况考虑的太复杂了,暂且先放过作者吧(-_-#)。

我们第一次写的单例模式是下面这个样子的:
/**  
 * 实现单例访问Kerrigan的第一次尝试  
 */  
public class SingletonKerriganA {   
    
    /**  
     * 单例对象实例  
     */  
    private static SingletonKerriganA instance = null;   
    
    public static SingletonKerriganA getInstance() {   
        if (instance == null) {                              //line A   
            instance = new SingletonKerriganA();          //line B   
        }   
        return instance;   
    }   
}   




这个写法我们把四点需求从上往下检测,发现第二点的时候就出了问题,假设这样的场景:两个线程并发调用SingletonKerriganA.getInstance(),假设线程一先判断完instance是否为null,既代码中的line A进入到line B的位置。刚刚判断完毕后,JVM将CPU资源切换给线程二,由于线程一还没执行line B,所以instance仍然是空的,因此线程二执行了new SignletonKerriganA()操作。片刻之后,线程一被重新唤醒,它执行的仍然是new SignletonKerriganA()操作,好了,问题来了,两个Kerrigan谁是李逵谁是李鬼?



紧接着,我们做单例模式的第二次尝试:

/**  
 * 实现单例访问Kerrigan的第二次尝试  
 */  
public class SingletonKerriganB {   
    
    /**  
     * 单例对象实例  
     */  
    private static SingletonKerriganB instance = null;   
    
    public synchronized static SingletonKerriganB getInstance() {   
        if (instance == null) {   
            instance = new SingletonKerriganB();   
        }   
        return instance;   
    }   
}  


比起第一段代码仅仅在方法中多了一个synchronized修饰符,现在可以保证不会出线程问题了。但是这里有个很大(至少耗时比例上很大)的性能问题。除了第一次调用时是执行了SingletonKerriganB的构造函数之外,以后的每一次调用都是直接返回instance对象。返回对象这个操作耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,因此从性能上说很不划算。

那继续把代码改成下面的样子:
/**  
 * 实现单例访问Kerrigan的第三次尝试  
 */  
public class SingletonKerriganC {   
    
    /**  
     * 单例对象实例  
     */  
    private static SingletonKerriganC instance = null;   
    
    public static SingletonKerriganC getInstance() {   
        synchronized (SingletonKerriganC.class) {   
            if (instance == null) {   
                instance = new SingletonKerriganC();   
            }   
        }   
        return instance;   
    }   
}  


基本上,把synchronized移动到代码内部是没有什么意义的,每次调用getInstance()还是要进行同步。同步本身没有问题,但是我们只希望在第一次创建Kerrigan实例的时候进行同步,因此我们有了下面的写法——双重锁定检查(DCL)。

/**  
 * 实现单例访问Kerrigan的第四次尝试  
 */  
public class SingletonKerriganD {   
    
    /**  
     * 单例对象实例  
     */  
    private static SingletonKerriganD instance = null;   
    
    public static SingletonKerriganD getInstance() {   
        if (instance == null) {   
            synchronized (SingletonKerriganD.class) {   
                if (instance == null) {   
                    instance = new SingletonKerriganD();   
                }   
            }   
        }   
        return instance;   
    }   
}  


看起来这样已经达到了我们的要求,除了第一次创建对象之外,其他的访问在第一个if中就返回了,因此不会走到同步块中。已经完美了吗?

我们来看看这个场景:假设线程一执行到instance = new SingletonKerriganD()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:

1.给Kerrigan的实例分配内存。

2.初始化Kerrigan的构造器

3.将instance对象指向分配的内存空间(注意到这步instance就非null了)。

但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三点,instance已经是非空了,所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来,真是一茶几的杯具啊。

DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonKerriganD instance = null;”就可以保证每次都去instance都从主内存读取,就可以使用DCL的写法来完成单例模式。当然volatile或多或少也会影响到性能,最重要的是我们还要考虑JDK1.42以及之前的版本,所以本文中单例模式写法的改进还在继续。

代码倒越来越复杂了,现在先来个返璞归真,根据JLS(Java Language Specification)中的规定,一个类在一个ClassLoader中只会被初始化一次,这点是JVM本身保证的,那就把初始化实例的事情扔给JVM好了,代码被改成这样:

/**  
 * 实现单例访问Kerrigan的第五次尝试  
 */  
public class SingletonKerriganE {   
    
    /**  
     * 单例对象实例  
     */  
    private static SingletonKerriganE instance = new SingletonKerriganE();   
    
    public static SingletonKerriganE getInstance() {   
        return instance;   
    }   
}  


好吧,如果这种写法是完美的话,那前面那么几大段话就是作者在消遣各位读者。这种写法不会出现并发问题,但是它是饿汉式的,在ClassLoader加载类后Kerrigan的实例就会第一时间被创建,饿汉式的创建方式在一些场景中将无法使用:譬如Kerrigan实例的创建是依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

再来看看下面这种我觉得能应对较多场景的单例写法:

/**  
 * 实现单例访问Kerrigan的第六次尝试  
 */  
public class SingletonKerriganF {   
    
    private static class SingletonHolder {   
        /**  
         * 单例对象实例  
         */  
        static final SingletonKerriganF INSTANCE = new SingletonKerriganF();   
    }   
    
    public static SingletonKerriganF getInstance() {   
        return SingletonHolder.INSTANCE;   
    }   
}  


这种写法仍然使用JVM本身机制保证了线程安全问题;由于SingletonHolder是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。

其他单例模式的写法还有很多,如使用本地线程(ThreadLocal)来处理并发以及保证一个线程内一个单例的实现、GoF原始例子中使用注册方式应对单例类需要需要继承时的实现、使用指定类加载器去应对多ClassLoader环境下的实现等等。我们做开发设计工作的时,应当既要考虑到需求可能出现的扩展与变化,也应当避免“幻影需求”导致无谓的提升设计、实现复杂度,最终反而带来工期、性能和稳定性的损失。设计不足与设计过度都是危害,所以说没有最好的单例模式,只有最合适的单例模式。

到这里为止,单例模式本身就先告一段落了,最后在介绍从其他途径屏蔽构造单例对象的方法:
1.直接new单例对象

2.通过反射构造单例对象

3.通过序列化构造单例对象。

对于第一种情况,一般我们会加入一个private或者protected的构造函数,这样系统就不会自动添加那个public的构造函数了,因此只能调用里面的static方法,无法通过new创建对象。

对于第二种情况,反射时可以使用setAccessible方法来突破private的限制,我们需要做到第一点工作的同时,还需要在在 ReflectPermission("suppressAccessChecks") 权限下使用安全管理器(SecurityManager)的checkPermission方法来限制这种突破。一般来说,不会真的去做这些事情,都是通过应用服务器进行后台配置实现。

对于第三种情况,如果单例对象有必要实现Serializable接口(很少出现),则应当同时实现readResolve()方法来保证反序列化的时候得到原来的对象。

基于上述情况,将单例模式增加两个方法:

/**  
 * 能应对大多数情况的单例实现  
 */  
public class SingletonKerrigan implements Serializable {   
    
    private static class SingletonHolder {   
        /**  
         * 单例对象实例  
         */  
        static final SingletonKerrigan INSTANCE = new SingletonKerrigan();   
    }   
    
    public static SingletonKerrigan getInstance() {   
        return SingletonHolder.INSTANCE;   
    }   
    
    /**  
     * private的构造函数用于避免外界直接使用new来实例化对象  
     */  
    private SingletonKerrigan() {   
    }   
    
    /**  
     * readResolve方法应对单例对象被序列化时候  
     */  
    private Object readResolve() {   
        return getInstance();   
    }   
}  



总结:

本章通过一次次的的尝试,去了解单例模式各种实现方案的优缺点。对双锁锁定检测进行了简单的讨论,相信大家能从各种尝试的演化过程中,明白为何单例模式是最简单而又最复杂的一种构造模式。

---------------volatile介绍-----------------
java的volatile是什么意思
我们知道,在Java中设置变量值的操作,除了long和double类型的变量外都是原子操作,也就是说,对于变量值的简单读写操作没有必要进行同步。

这在JVM 1.2之前,Java的内存模型实现总是从主存读取变量,是不需要进行特别的注意的。而随着JVM的成熟和优化,现在在多线程环境下volatile关键字的使用变得非常重要。

在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

要解决这个问题,只需要像在本程序中的这样,把该变量声明为volatile(不稳定的)即可,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。

Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。

这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。

而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。

使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。

由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

注:我在单例模式中用到此关键字.

$$ 单例的一个例子:

public class Singleton(){
     private volatile static Singleton singleton;
     private Sington(){};
     public static Singleton getInstance(){
     if(singleton == null){
         synchronized (Singleton.class);
              if(singleton == null){
                 singleton = new Singleton();
                                   }
                          }
              }
return singleton;
        }
}
分享到:
评论
4 楼 qiaoenxin 2013-12-10  
那么不使用volatile关键字能否实现安全DCL呢?答案当然是肯定的,有些文章说,在jdk1.5之前(1.5之前没有volatile关键字),无法实现DCL的singleton,这种说法是错误的。废话少说,上代码 
------------------------------------- 
public class Singleton {   
   
    private static Singleton singleton; // 这类没有volatile关键字   
   
    private Singleton() {   
    }   
   
    public static Singleton getInstance() {   
        // 双重检查加锁   
        if (singleton == null) {   
            synchronized (Singleton.class) {   
                // 延迟实例化,需要时才创建   
                if (singleton == null) {   
                       
                    Singleton temp = null; 
                    try { 
                        temp = new Singleton();   
                    } catch (Exception e) { 
                    } 
                    if (temp != null)   
                        singleton = temp; //为什么要做这个看似无用的操作,因为这一步是为了让虚拟机执行到这一步的时会才对singleton赋值,虚拟机执行到这里的时候,必然已经完成类实例的初始化。所以这种写法的DCL是安全的。由于try的存在,虚拟机无法优化temp是否为null 
                }   
            }   
        }   
        return singleton;   
    } 
}  
3 楼 lhn1989 2013-10-28  
2楼的代码,当JIT编译器看到加粗字体段的代码时,会进行优化,结果就会有如下代码出现:
synchronized (RequestManager.class) {
                RequestManager instance = mInstance;
                if (instance == null) {
                    mInstance = new RequestManager(context);
                }
            }
当遇到这样的优化时,问题又变得和之前一样了。

另外LZ,“饿汉式的创建方式在一些场景中将无法使用:譬如Kerrigan实例的创建是依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。”
这段话能否贴段设置参数的代码?
2 楼 longxiaozhi 2013-10-16  
public class RequestManager {
    public final static String TAG = "ApiManager";

    private static RequestManager mInstance;
    private RequestQueue mRequestQueue;

    public static RequestManager getInstance(Context context) {
        if (mInstance == null) {
           synchronized (RequestManager.class) {
                RequestManager instance = mInstance;
                if (instance == null) {
                    instance = new RequestManager(context);
                }
                mInstance = instance;

            }
        }
        return mInstance;
    }
1 楼 seoltsubasa 2011-12-29  
你好:

2.通过反射构造单例对象


我想问:为了防止2.这种情况,单例类里面还应该怎么写呢。

我意思是用代码实现。


谢谢哈!

相关推荐

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

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

    设计模式之单例模式(结合工厂模式)

    单例模式的核心在于控制类的实例化过程,常见的实现方式有懒汉式、饿汉式和双重检查锁定(DCL)等。懒汉式是在类被加载时并不创建实例,而是在第一次调用getInstance方法时才创建;饿汉式则是在类加载时就创建实例,...

    C#单例模式详解 C#单例模式详解C#单例模式详解

    4. 双重检查锁定(DCL,线程安全): 使用volatile关键字保证线程可见性和避免指令重排序,确保多线程环境下的安全性。 ```csharp public class Singleton { private static volatile Singleton instance; ...

    7种单例模式

    4. **双重检查锁定(DCL)单例**: 结合了懒汉式的延迟初始化和饿汉式的线程安全性,更高效。 ```java public class Singleton { private volatile static Singleton instance; private Singleton() {} public...

    23钟设计模式之单例模式

    单例模式有多种实现方式,常见的包括懒汉式、饿汉式以及双重检查锁定(DCL)等。 1. 懒汉式:懒汉式是在第一次使用时才创建单例对象,以延迟加载提高效率。但是,如果在多线程环境下,没有正确处理同步问题,可能...

    使用单例模式创建学生管理系统(饿汉式、懒汉式)

    但是,如果不进行同步控制,懒汉式在多线程环境下可能会创建多个实例,因此通常采用双重检查锁定(Double-Checked Locking,DCL)来实现线程安全的懒汉式单例: ```java public class Singleton { private ...

    设计模式——单例模式(懒汉模式)

    在Java中,懒汉式的单例模式通常通过双重检查锁定(Double-Check Locking,DCL)来实现,这是一种线程安全的方式。下面是一个简单的DCL实现的懒汉式单例模式代码示例: ```java public class Singleton { private ...

    几种单例模式demo

    单例模式是软件设计模式中的一种,它的主要目的是确保一个类只有一个实例,并提供一个全局访问点。这种模式在很多场景下都非常有用,比如控制资源的唯一性、管理共享配置或者创建昂贵的对象时避免频繁创建销毁。 ...

    设计模式之单例模式源码demo

    3. **双重检查锁定(DCL,Double Check Locking)单例模式**:结合了饿汉模式和懒汉模式的优点,既延迟了初始化,又保证了线程安全。在多线程环境下,只有在`instance`为`null`时才会进入同步块,避免了不必要的同步...

    Java 单例模式 工具类

    Java中的单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供全局访问点。在Java编程中,单例模式常用于控制资源的访问,比如数据库连接池、线程池或者日志对象等。本篇文章将深入探讨如何在Java中...

    设计模式单例模式

    单例模式是软件设计模式中的一种基础且广泛应用的模式,它的主要目的是确保一个类只有一个实例,并提供一个全局访问点。这种模式在系统中需要频繁创建和销毁对象,且对象创建成本较高,或者需要共享资源的情况下非常...

    单例模式的要点及模式

    单例模式是软件设计模式中的一种经典模式,其核心思想在于限制类的实例化过程,确保在整个应用程序中只有一个实例存在。这种模式常用于管理共享资源,例如数据库连接、线程池或者全局配置等,以避免资源浪费和状态...

    单例模式各种实现方式

    3. **双重检查锁定(DCL,线程安全)**: 在`getInstance`方法中添加了双重检查,确保了线程安全。 ```java public class Singleton { private volatile static Singleton instance; private Singleton() {} ...

    大话设计模式--Singleton(单例模式)

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

    Java单例模式深入理解

    Java单例模式是一种设计模式,它允许在程序中创建唯一一个类实例,通常用于管理共享资源,例如数据库连接、线程池或者配置对象等。单例模式的核心在于限制类的构造函数,确保类只能被初始化一次,从而实现全局唯一的...

    单例模式Java实现

    3. **双重检查锁定(DCL,线程安全)**:在多线程环境下,通过双重检查锁定来确保线程安全,避免了同步带来的性能影响。 ```java public class Singleton { private volatile static Singleton instance; private ...

    单例模式代码.rar

    4. **双重检查锁定(DCL,线程安全)**:解决了懒汉式的线程安全问题,通过双重检查和同步来确保单例的正确创建。 ```java public class Singleton { private volatile static Singleton INSTANCE; private ...

    C#版本的单例模式源码

    2. **双检锁/双重校验锁定(Double-Checked Locking,DCL)** 这种方法在早期的.NET框架中被广泛使用,但后来由于.NET内存模型的变化,其线程安全性变得复杂。在.NET 4.0及更高版本中,可以安全地使用此方法: ``...

    五种单例模式

    3. **双重检查锁定(DCL)单例**: 这种方式结合了懒汉式和饿汉式的优点,既延迟了实例化,又保证了线程安全。通过两次检查instance是否为null来避免不必要的同步。 ```java public class Singleton { private ...

    设计模式-单例模式

    5. **双重检查锁定(DCL,线程安全)**: 解决了懒汉式线程安全与效率问题,但在JDK1.5之前可能存在隐患,1.5之后已修复。 ```java public class Singleton { private volatile static Singleton instance; ...

Global site tag (gtag.js) - Google Analytics