单例设计模式

定义

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

使用场景

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

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

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;
}
总结

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值