单例模式(一)

1. 定义

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

2. UML类图

Singleton.png

角色介绍:

  1. Client-高层客户端
  2. Singleton-单例类
    实现单例模式有以下几个关键点:
  • 构造函数私有
  • 提供一个静态方法或者枚举返回单例类对象
  • 确保单例类对象有且只有一个,尤其在多线程的环境下
  • 确保单例类对象在反序列化时不会重新构建对象

3. 单例模式实现方式

3.1 懒汉模式

懒汉模式是声明一个静态对象,并且在用户第一次调用getInstance时进行初始化。实现方式如下:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

getInstacne()方法中添加了synchronized关键字,也就是说getInstance()是一个同步方法,这就是上面所说的在多线程情况下保证单例对象唯一性的手段。我们细想一下,会发现即使instacne已经被初始化完成,每次调用getInstance()方法都会进行同步,这样就会造成资源的不必要消耗,这也是懒汉单例模式存在的最大问题。
最后总结一下:懒汉单例模式的优点是只有在使用时才会被实例化,在一定程度上节约了资源。最大的问题是每次调用getInstance()都进行同步,造成不必要的同步开销。一般不推荐使用

3.2 DCL(Double check Lock)单例模式

DCL方式实现的单例模式优点是既能在需要时才初始化单例,又能保证线程安全,且单例对象初始化后调用getInstance不进行同步锁。

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

        }
        return instance;
    }
}

从代码中可以看到在getInstance()方法中,最instance进行了两次判空。第一次判空主要是为了避免不必要的同步,第二层判空是为了再null情况下创建实例。我们来分析一下:
假设线程A执行到instance=new Singleton()语句时,这里看起来是一句代码,但实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了三件事情:

  1. 给Singleton的实例分配一块内存空间;
  2. 调用Singleton()的构造函数,初始化成员变量;
  3. 将instance对象指向分配的内存空间,此时instance就不是NULL了。

但是,由于JAVA编译器允许处理器乱序执行,以及JDK1.5 之前Java内存模型中Cache、寄存器到主内存回写顺序的规定,上面的2和3的顺序是无法保证的,也就是说执行顺序可能是1-2-3,也有可能是1-3-2。如果是后者,并且在第3步执行完毕,2未执行前,被切换到线程B上,这时instance因为以及在线程A内执行过了第3点,instance已经是非空了,所以线程B直接取走instance,再使用时就会出错,这就是DCL失效问题,而且这种难以控制跟踪难以重现的错误很可能会隐藏很久。
在JDK1.5之后,SUN官方已经注意到这种问题,优化了JVM,具体化了volatile关键字,因此,在JDK的版本大于1.5时,只需要将instance实例变量增加volatile修饰符。这样可能保证instance对象每次都是从主存中读取。虽然增加volatile会多少的影响性能,考虑到程序的正确性,这点影响是值得的。修改后的DCL单例模式代码如下:

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

DCL的优点:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。缺点是第一次加载时反应慢。也由于JAVA内存模型的原因偶尔会失败,在高并发环境下也有一定的缺陷。在JDK高于1.5版本时,这种方式能够满足一般需求的。

3.3 静态内部类的单例模式

DCL虽然在在一定程度上解决了资源消耗、多余同步、线程安全等问题,但是,它还是在某些情况下出现失效的问题。这个问题被称为DCL失效。在《JAVA并发编程实践》一书的最后谈到了这个问题,并指出这种“优化”是丑陋的,不赞成使用。而建议使用如下代码代替:

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

当第一次加载Singleton类时并不会初始化instance,只有在第一次调用Singleton的getInstance()方法时才会导致instance初始化。因此,第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能够保证线程安全,也能够保证单例对象的唯一性,同时也延时了单例的实例化,所以推荐使用这种单例模式。

3.4 枚举单例

public enum Singleton {
    INSTANCE
}

是不是超级简单,这就是枚举单例模式。枚举与Java中的普通类时一样的,不仅能够有字段,还可以有自己的方法。最重要的是默认枚举实例的创建时线程安全的,并且在任何情况下它都是一个单例。
为什么这么说呢?在上述几种单例模式的实现中,在一个情况下它们会出现重新创建对象的情况,那就是反序列化!
通过反序列化可能将一个单例对象写到磁盘,然后再读回来,从而有效地获得一个实例。即使构造函数私有化,在反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供一个很特别的钩子函数,类中具有一个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。例如,上述几个单例模式中,要杜绝单例对象在被反序列化的时候重新生成对象,那么必须加入以下方法:

private Object readResolve() throws ObjectStreamException{
        return instance;
}

也就是在readResovle方法中将instance对象返回,而不是默认的重新生成一个新的对象,对于枚举,并不会出现这个问题,因为即使反序列化它也不会重新生成新的实例。

最后,不管哪种形式实现单例模式,核心原理都是将构造函数私有化,并提供静态方法获取一个唯一实例。具体使用哪种形式,应取决于项目的本身。

参考:《Android源码设计模式解析与实战》

你可能感兴趣的:(单例模式(一))