单例设计模式

来源:互联网 发布:mac访客模式 编辑:程序博客网 时间:2024/06/05 02:35

定义

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

使用场景

确保某个类有且只有一个对象,避免产生更多的对象消耗过多的资源。

实现步骤

  • 构造函数私有
  • 通过一个静态方法或枚举返回该类的实例
  • 确保在多线程的情况下,该类的对象有且只有一个
  • 确保该类在反序列化时不会重新构建对象

实现方式

1 . 饿汉式

public class Singleton {    private static final Singleton mSingleton = new Singleton();    //构造函数私有    private Singleton() {}    //对外暴露获取实例的方法    public static Singleton getInstance() {        return mSingleton;    }}

优缺点:该方式在类加载时就创建实例,需要用到时直接可以获取,并且在多线程中也可以保证有且只有一个实例。缺点也是显而易见的,在获取该实例之前,对象就已经被创建并保存在内存中,耗费了不必要的资源。

2 . 懒汉式

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

优缺点:可以看到,懒汉式对饿汉式的缺点进行了改进,在类加载的时候不进行初始化,当需要用到该实例时再进行初始化,这样就在一定程度上避免了资源的浪费。

我们可以注意到,在 getInstance() 方法上加了同步锁,这是为了保证在多线程调用的情况下,保证有且只有一个实例。

看似该方式已经很完美了,但是还有一种情况,如果第一次调用后,mSingleton 已经被初始化了,那么后续调用 getInstance() 方法的时候每次都要经过同步锁,这样也会消耗不必要的资源。

3 . Double Check Lock (DCL)

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

优缺点:这种方式通过对 mSingleton 的双重判空,弥补了上一种方式的缺陷。第一层判断是为了避免不必要的同步,第二层判断是为了在 mSingleton 为空的时候创建实例。如此,这种方式可以保证在使用时再创建实例,节省了内存的消耗,也避免了因同步造成的资源损耗,是目前最优的单例模式实现方式。这种方式也是最常被使用的。

但是,这种方式也并不是绝对完美的。如果是在高并发的环境下,也可能会出错。接下来我们来分析:

mSingleton = new Singleton() 看上去只是一句代码,实际上它并不具备原子性,这句代码会被编译成多条汇编指令,它大概会进行三个动作

  • 给 mSingleton 的实例分配内存
  • 调用 Singleton() 的构造函数,进行初始化操作
  • 将 mSingleton 对象指向分配的内存空间(该步骤执行后 mSingleton 就不为空了)

由于 Java 编译器允许处理器乱序执行,以及 JDK1.5 之前 Java 内存模型中 Cache 、寄存器到主内存回写顺序规定,上述第二步和第三步的执行顺序是无法确定的。所以,可能会存在这样一种情况,A 线程在执行完上述第三步之后,此时 Singleton 的构造函数还没有执行,数据没有初始化,但是 mSingleton 已经不为空了。此刻 B 线程执行第一层非空判断时,就会直接返回 mSingleton 实例,这时就会出错。

在 JDK1.5 之后,可以给 mSingleton 添加 volatile 关键字,这样就可以保证 mSingleton 对象每次都是从主内存中获取,可以避免以上情况的发生。当然,以上这种出错情况发生的几率很低,如果单例的使用场景比较简单,而且不存在高并发,完全可以忽略这种错误情况。

4 . 静态内部类

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

这种方式也实现了 mSingleton 实例的延迟初始化,并且是线程安全的,所以也是推荐使用的一种方式。

5 . 使用 Map 存储实例

public class SingletonManager {    private static Map<String, Object> objMap = new HashMap<String, Object>();    private SingletonManager() {}    public static void registerService(String key, Object instance) {        if(!objMap.containsKey(key)) {            objMap.put(key, instance);        }    }    public static Object getService(String key) {        return objMap.get(key);    }}

在系统的使用过程中,将不同的单例类的实例存入 Map 中,使用时,通过 key 来获取对应的实例。该方式适用于同一系统中存在多个单例类的情况,可以统一管理,降低耦合度。

以上单例的实现方式还有一个缺陷,就是在反序列化的时候,可能会重新创建实例,为了防止这种情况的发生,我们可以手动返回我们自己创建的 实例,避免了反序列化时默认生成新的实例,可以加入如下方法:

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

总结

单例模式,不管以什么样的方式来实现,思想都是一样的。就是构造函数私有化,通过提供公共的静态方法来获取唯一的实例,保证在多线程的情况下实例的唯一性。具体使用哪种方式实现,要根据项目的具体情况来定,以上方式仅供参考。

原创粉丝点击