创建型 — 单例模式(Singleton)

为什么需要单例模式

  单例模式是自己最先接触的一种设计模式,当时还是开发C++的代码。当时的应用场景是一个控制台程序,对于一个管理资源的类,也涉及初始化、启动等,这样的类只适合构造一个实例,然后不断的复用,保证在运行进程内只有一个实例,便于管理;同时也能减少资源的开销。

  从面向对象的概念上讲,我们知道封装,将相关联的东西封装成一个类。在Java中,一种最简单的模式就是POJO(或者叫Java Bean),包括一些属性和对应的getter和setter方法。这种是对属性的封装,通常是比较明显的对应一些实体,他们提供对数据的封装。另外的,就是更常见的类,因为面向对象编程,特别是在Java中,类似的操作或者语义都放在一个类中。

  当需要使用类中的方法时,可以声明一个类的实例,每一个类的实例都拥有这个类的非static属性的一份拷贝,方法也是类似。通常的做法是,在需要用的地方声明一个实例,然后用实例去调用对应的方法。但是有的时候,希望这个类只存在一个实例,也就是这个类中的属性只有一份状态,在各处使用时都是对那一份数据的操作。

简单的单例模式

在Java中,常见的模式如下:

public class Singleton {

    //私有,静态的一个实例
    private static Singleton instance = new Singleton();

    // 必须得实现一个私有的无参构造函数,防止调用方直接new实例
    private Singleton() {}

    // 供使用者调用
    public static Singleton getInstance() {
        return instance;
    }
}

  类的使用者要想使用这个类,只有一种方法得到这个类的实例;并且在类的内部,这个实例是静态的,只存在一份。因此,这就保证了“单例”。注意,这样的写法也是线程安全的。

  但这中方法有一个问题,就是在类加载时就要初始化,如果如下初始化的内容比较多,加载起来就会比较慢,因此有了新的方法——延迟初始化(Lazy Initialize),如下:

public class Singleton {

    //私有,静态的一个实例,先不初始化
    private static Singleton instance;

    // 必须得实现一个私有的无参构造函数,防止调用方直接new实例
    private Singleton() {}

    // 供使用者调用
    public static Singleton getInstance() {
        if (instance != null) {
            instance = new Singleton();
        }
        return instance;
    }
}

但是这样又出现了一个问题,就是会导致线程不安全!

其他方案

  但是常见的模式在并发环境中会出现问题。当多个线程同时调用getInstance()方法时,有可能某个线程得到的instance还没有被初始化,这样将不会得到一个初始化好的实例。首先想的的方法是加同步。

  这个时候通常还结合另一种技术——延迟初始化,也就是不采用简单单例模式中的,在声明变量是就初始化的方式,而是等到要用时在进行初始化。同时,还会加上双重检查,得到的通常模式如下:

class UnSafeSingleton {
    private static UnSafeSingleton instance;

    private UnSafeSingleton() {}

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

  可以看到,这种方式加上了同步处理,并且进行了延迟初始化,稍微不注意,以为这个会是线程安全的。但是,这边还是有问题,就是在给instance赋值时,还是会出现不一致的问题。

  一种解决办法是,给instance属性加上volatile特征,这样保证instance对各个线程的可见性,保证只有一份存在。

  另一种方法是采用基于类初始化的方法。JVM在类的初始化阶段(即在Class在被加载后,且被线程使用之前),会执行类的初始化。

  在执行类的初始化期间,JVM回去获得一个锁。这个锁可以同步多个线程对同一个类的初始化。代码如下:

class SafeSingleton {

    private SafeSingleton() {}

    private static class SafeSingletonHolder {
        public static SafeSingleton intance = new SafeSingleton();
    }

    public static SafeSingleton getInstance() {
        return SafeSingletonHolder.intance;
    }
}

  这样,即使多个线程去调用getInstance()方法,但是在初始化类SafeSingletonHolder时,都能同步进行,因此可以保证线程安全性。

小结

  综上所述,单例模式本身很简单,但是如果要是线程安全,还是得有很多考虑。通过比较发现,基于类初始化的方法比较简单,代码简洁,可以常用。但是基于volatile的双重检查锁定方法,在延迟初始化非静态实例字段时,唯一可用。

补充

最近看了些《Effective Java》,发现在第一章第3条,讲到单例模式时,只列举了以下三种方式:

  1)直接定义public static final Single INSTANCE= new Single(),此instance供外界直接调用。同时得有私有构造函数。

  2)instance变为私有,但也是定义时初始化,对外提供一个接口,public static Single getInstance() { return INSTANCE; },这种方式也是书中所讲的工厂方法。

  3)单元素的枚举类型。public enum Single { INSTANCE; }

  作者在将前两种方式时,都强调,享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造函数。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。

  当然,要想更简单的防止反射攻击,作者也写明,直接利用单元素枚举类型就可以天然的解决。

  

  至于延迟初始化,作者放在了后面去讲,第71条"慎用延迟初始化"。作者觉得除非万不得已,就不用。同时给出两条建议。

  1)如果出于性能考虑,而需要对静态域使用延迟初始化,就使用lazy initialization holder class模式。

  2)如果处于性能考虑,而需要对实例域使用延迟初始化,就使用双重检查模式(doublecheck idiom)。域要声明为volatile。

  也可以看出,如果是想用延迟初始化实现单例模式,建议的方式是lazy initialization holder class模式!!

 

posted @ 2016-03-08 10:37  luceion  阅读(242)  评论(0编辑  收藏  举报