微信扫一扫

028-83195727 , 15928970361
business@forhy.com

单例模式之七种表现方式

单例2016-07-05

对于单例模式,有点工作经验的人都使用过,原本未打算去写这块的,感觉多此一举,虽然写博客最主要的目的是给自己做笔记,但由于种种原因,决定把这个最基础的设计模式拿出来写成两篇博客,第一篇叙述七种单例,第二篇叙述单例中的坑,主要是双重锁的滥用和扩展思考。
使用单例的优点:

  • 单例类只有一个实例
  • 共享资源,全局使用
  • 节省创建时间,提高性能

七种表现方式

  • 饿汉式
public class Singleton {

    private static final Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;

}

这种方式和名字很贴切,饥不择食,在类装载的时候就创建,不管你用不用,先创建了再说,如果一直没有被使用,便浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。

昨天无意间看到runtime就是使用这种方式,代码如下:

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() {}
    //以下代码省略
}
  • 懒汉式(非线程安全)
public class Singleton {  
      private static Singleton instance;  
      private Singleton (){
      }   
      public static Singleton getInstance() {  
      if (instance == null) {  
          instance = new Singleton();  
      }  
      return instance;  
      }  
 }
  • 懒汉式(线程安全)
public class Singleton {  
      private static Singleton instance;  
      private Singleton (){
      }
      public static synchronized Singleton getInstance() {  
      if (instance == null) {  
          instance = new Singleton();  
      }  
      return instance;  
      }  
 }

这两种懒汉式单例,名字起的也很贴切,一直等到对象实例化的时候才会创建,确实够懒,不用鞭子抽就不知道走了,典型的时间换空间,每次获取实例的时候才会判断,看是否需要创建,浪费判断时间,如果一直没有被使用,就不会被创建,节省空间。

第一种方式,在多线程访问的时候,很可能会造成多次实例化,就不再是单例了,第二种方式,在多线程情况下会造成线程阻塞,把大量的线程锁在外面,只有一个线程执行完毕才会执行下一个线程。

  • 双重检验(DCT)
/**
 * 
 * created by zero on 2016-07-05
 * 
 */
public class Singleton
{
    /**
     * 在一些博客上面看到双重检验,有的会使用volatile,我是觉得有必要使用volatile,
     * 被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
     * 对于volatile关键字有疑惑的的,可以看一些相关的帖子,此关键字日后会单独拿出来讲述
     */
    private volatile static Singleton singleton;

    // private static Singleton singleton;

    private Singleton()
    {
    }

    public static Singleton getInstance() {
        if (singleton == null)
        {
            synchronized (Singleton.class)
            {
                if (singleton == null)
                {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

我是挺喜欢这种方式,理由有二,既可以达到线程安全,也可以使性能不受很大的影响,换句话说在保证线程安全的前提下,既节省空间也节省了时间,集合了饿汉式和两种懒汉式的优点,取其精华,去其槽粕。

所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

对于volatile关键字,还是存在很多争议的,上述也提到被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。但是由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。还有点就是在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java1.5及以上的版本。

  • 静态内部类
public class Singleton { 
    private Singleton(){
    }
      public static Singleton getInstance(){  
        return SingletonHolder.sInstance;  
    }  
    private static class SingletonHolder {  
        private static final Singleton sInstance = new Singleton();  
    }  
}

第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder 并初始化sInstance ,这样不仅能确保线程安全也能保证Singleton类的唯一性,在《Java并发编程实践》一书建议用静态内部类单例模式来替代DCL。因为此书比较老,我查了下,是2007年出版的,上述也说了,在Java1.4之前会出现失效的问题。查了一些资料后发现,在Java1.5以后,也是会出现失效的情况,最近在整理synchronized 相关的知识点,接下来会在synchronized 中详细叙述失效的情况。

  • 枚举
public enum Singleton {  
     INSTANCE;  
     public void doSomeThing() {  
     }  
 }  

枚举,《Effective Java》作者推荐使用的方法,优点:不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。在《Java与模式》中,作者这样写道,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。
枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,对于枚举,我在学C语言的时候会偶尔用用,在Android的三年多里面,自己几乎没有写过。

  • 使用容器实现单例模式
public class SingletonManager { 
  private static Map<String, Object> objMap = new HashMap<String,Object>();
  private Singleton() { 
  }
  public static void registerService(String key, Objectinstance) {
    if (!objMap.containsKey(key) ) {
      objMap.put(key, instance) ;
    }
  }
  public static ObjectgetService(String key) {
    return objMap.get(key) ;
  }
}

用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

对于这七种单例,很多时候取决人个人的喜好,虽然双重检查有一定的弊端和问题,但我就是一种钟爱双重检查,觉得这种方式可读性高、安全、优雅(个人观点)。

参考文献:《Java与模式》
参考博客:http://blog.csdn.net/itachi85/article/details/50510124