浅谈设计模式--单例模式(Singleton Pattern)

题外话:很久没写blog,作知识概括整理了。原本设计模式就是个坑,各类文章也写烂了。不过,不是本身写的东西,缺乏点知识的存在感。目前还没作到光看即能记住,得写。因此准备跳入设计模式这个大坑。html

 

开篇先贡献给java

单例模式(Singleton Pattern)

 

目的:保证一个类仅有一个实例,并提供一个访问它的全局访问点。程序员

其实单例模式应用不少,我也不陌生,有时候一些本身定义的Controller等,都会选择单例模式去实现,而自己java.lang.Runtime类的源码也使用了单例模式(Jdk7u40):编程

public class Runtime {

    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() { 
      return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    
    ......

}

然而,由于涉及到多线程编程,单例模式仍是有很多值得注意的地方,请看下面的各类实现。设计模式

 

1.最简单实现:

/**
 * @author YYC
 * lazy-loading but NOT thread-safe
 */
public class SingletonExample {

    private static SingletonExample instance;
    
    private SingletonExample(){}
    
    public static SingletonExample getInstance(){
      if(instance==null){
          instance = new SingletonExample();
      }
      return instance;
    }
}

这是单例模式最简单最直接的实现方法。懒汉式(lazy-loading)实现,但缺点很明显:线程不安全,不能用于多线程环境安全

 

2.同步方法实现:

/**
 * @author YYC
 * Thread-safe but bad performance
 */
public class SingletonExample {

    private static SingletonExample instance;
    
    private SingletonExample(){}
    
    public static synchronized SingletonExample getInstance(){
      if(instance==null){
          instance = new SingletonExample();
      }
      return instance;
    }
}

同步getInstance()这个方法,能够保证线程安全。不过代价是性能会受到,由于大部分时间的操做其实不须要同步。多线程

 

3. Double-Checked Locking实现(DCL):

/**
 * @author YYC
 * Double-Checked Locking
 */
public class SingletonExample {

    private static SingletonExample instance;
    
    private SingletonExample(){}
    
    public static  SingletonExample getInstance(){
      if(instance==null){
          synchronized(SingletonExample.class){
            if(instance==null){
                instance = new SingletonExample();
            }
          }
      }
      return instance;
    }
}

直接同步整个getInstance()方法产生性能低下的缘由是,在判断(instance==null)时,全部线程都必须等待。而(instance==null)并不是是常有状况,每次判断都必须等待,会形成阻塞。所以,有了这种双重检测的实现方法,待检查到实例没建立后(instance=null),再进行同步,而后再检查一次确保实例没建立。框架

 

在同步块里,再断定一次,是为了不线程A准备拿到锁,而线程B建立完instance后准备释放锁的状况。若是在同步块里没有再次断定,那么线程A极可能会又建立一个实例。性能

另外,再引用IcyFenix文章里面的一段话,会解释清楚双锁检测的局限性:测试

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

 

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

2.初始化SingletonExample的构造器

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是可行的,取决因而否能保证二、3步的顺序。在JDK1.5以后,官方已经注意到这种问题,所以调整了JMM、具体化了volatile关键字,所以若是JDK是1.5或以后的版本,只须要将instance的定义改为“private volatile static SingletonExample instance = null;”就能够保证每次都去instance都从主内存读取,就可使用DCL的写法来完成单例模式。固然volatile或多或少也会影响到性能,最重要的是咱们还要考虑JDK1.42以及以前的版本,因此本文中单例模式写法的改进还在继续。

 

4. 饿汉式实现(Hungry man):

/**
 * @author YYC
 * Hungry man. Using class loader to make it thread-safe 
 */
public class SingletonExample2 {

    private static SingletonExample2 instance = new SingletonExample2();
    
    private SingletonExample2(){}
    
    public static SingletonExample2 getInstance(){
      return instance;
    }
    
}

根据Java Language Specification,JVM自己保证一个类在一个ClassLoader中只会被初始化一次。那么根据classloader的这个机制,咱们在类装载时就实例化,保证线程安全。

可是,有些时候,这种建立方法并不灵活。例如实例是依赖参数或者配置文件的,在getInstance()前必须调用某些方法设置它的参数。

 

5. 静态内部类实现(static inner class):

/**
 * @author HKSCIDYX
 * static inner class: make it thread-safe and lazy-loading
 */
public class SingletonExample3 {

    private SingletonExample3(){}
    
    public static SingletonExample3 getInstance(){
      return SingletonHolder.INSTANCE;
    }
    
    private static class SingletonHolder{
      final static SingletonExample3 INSTANCE = new SingletonExample3();
    }
    
}

利用classloader保证线程安全。这种方法与第四种方法最大的区别是,就算SingletonExample3类被装载了,instance不必定被初始化,由于holder类没有被主动使用。相比而言,这种方法比第四种方法更加合理。

 

6. 枚举实现(Enum):

《Effective Java, 2nd》第三条:enum是实现Singleton的最佳方法

/**
 * @author HKSCIDYX
 * Enum
 */
public enum SingletonExample4 {

    INSTANCE;
    
    public void whateverMethod(){
    
    }
    
}

这种作法,其实还没真正在项目或者工做中见过。根据《Effective Java, 2nd》第三条,这种实现方法:

1. 简洁

2. JVM能够保证enum类的建立是线程安全(意味着其它方法的线程安全得由程序员本身去保证),

3. JVM能够无偿提供序列化机制。传统的单例模式实现方法都有个问题:一旦实现了serializable接口,他们就再也不是单例的了。由于readObject()方法总会返回一个新的实例。所以为了维护并保证单例,必须声明全部实例域都是transient的,且提供一个readRevolve()方法:

/**
 * 
 * @author HKSCIDYX
 * Handle Serialized situation
 */
public class SingletonExample5 implements Serializable{

    private static final long serialVersionUID = 1L;
    
    private static SingletonExample5 INSTANCE = new SingletonExample5();
    
    //if there's other states to maintain, it must be transient
    
    private SingletonExample5(){}
    
    public static SingletonExample5 getInstance(){
      return INSTANCE;
    }
    
    private Object readResolve(){
      return INSTANCE;
    }
    
}

 

 总结

 1. 单例模式,并非整个程序或者整个应用只有一个实例,而是整个classloader只有一个实例。若是单例由不一样的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每一个servlet使用彻底不一样的类装载器,这样的话若是有两个servlet访问一个单例类,它们就都会有各自的实例

2. 单例模式,会使测试、找错变得困难(根据《Effective Java, 2nd,第三条》) ,尝试使用DI框架(Juice/Spring)来管理。

3. 什么状况下单例模式会失效(JPMorgan)?

Serialization, Reflection, multiple ClassLoader, multiple JVM, broken doubled checked locking(JDK4 or below) etc

参考:

《Effective Java, 2nd》

《设计模式解析,2nd》

http://icyfenix.iteye.com/blog/575052

http://xuze.me/blog/2013/01/31/singleton-pattern-seven-written/

http://837062099.iteye.com/blog/1454934

http://javarevisited.blogspot.hk/2011/03/10-interview-questions-on-singleton.html

相关文章
相关标签/搜索