单例模式

单例模式(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是可行的,取决于是否能保证2、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

posted @ 2016-10-22 19:48  王哈哈、  阅读(243)  评论(0编辑  收藏  举报