单例模式
单例模式定义
- 确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式是“创建型”设计模式之一。
单例模式的使用场景
- 确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对
象只应该有且只有一个。例如,创建一个对象需要消耗的资源过多,如要访问I0和数据库等资源,这时就要考虑使用单例模式。
单例模式UML类图
角色介绍:
- Client高层客户端;
- Singleton单例类。
实现单例模式主要有如下几个关键点:
- 构造函数不对外开放,一般为Private;
- 通过一个静态方法或者枚举返回单例类对象;
- 确保单例类的对象有且只有一个,尤其是在多线程环境下;
- 确保单例类对象在反序列化时不会重新构建对象。
通过将单例类的构造函数私有化,使得客户端代码不能通过new的形式手动构造单例类的对象。单例类会暴露一个公有静态方法,客户端需要调用这个静态方法获取到单例类的唯一对象,在获取这个单例对象的过程中需要确保线程安全,即在多线程环境下构造单例类的对象也是有且只有一个,这也是单例模式实现中比较困难的地方。
单例类的实现
1. 饿汉模式
饿汉模式 - java版本
public class SimpleSingleton {
private static SimpleSingleton mInstance = new SimpleSingleton();
private SimpleSingleton() {
}
public static SimpleSingleton getInstance() {
return mInstance;
}
}
饿汉模式 - kotlin版本
object SimpleSingleton {
fun dosomething() {
println("dosomething")
}
}
饿汉式优缺点
优点:
- 在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。
缺点:
- 如果构造方法中存在过多的处理,会导致加载这个类时比较慢,可能引起性能问题。
- 如果使用饿汉式的话,只进行了类的装载,并没有实质的调用,会造成资源的浪费。
适用场景:
- 这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。
2. 懒汉模式
懒汉模式 - java版本
public class LazySingleton {
private static LazySingleton mInstance = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (null == mInstance) {
mInstance = new LazySingleton();
}
return mInstance;
}
}
懒汉模式 - kotlin版本
class LazySingleton {
companion object {
val mInstance: LazySingleton by lazy { LazySingleton() }
}
}
说明:
- 显式声明构造方法为private
- companion object用来在class内部声明一个对象
- LazySingleton的实例instance 通过lazy来实现懒汉式加载
- lazy默认情况下是线程安全的,这就可以避免多个线程同时访问生成多个实例的问题
懒汉式优缺点
优点:
- 懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。
缺点:
- java版本的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题(getInstance()方法上加上synchronized字段)。
适用场景:
- 如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。
3. 双重校验锁
双重校验锁版本
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton mInstance = null;
private DoubleCheckSingleton() {
}
public static DoubleCheckSingleton getInstance() {
if (mInstance == null) {
synchronized (DoubleCheckSingleton.class) {
if (mInstance == null) {
// Double checked
mInstance = new DoubleCheckSingleton();
}
}
}
return mInstance;
}
}
- 双重校验锁解决了“懒汉式”单例模式的线程安全问题。
- 增加了 volatile 关键字,禁止指令重排序优化。
4. 静态内部类
静态内部类版本
public class StaticInnerSingleton {
private static class SingletonHolder {
static StaticInnerSingleton mInstance = new StaticInnerSingleton();
}
private StaticInnerSingleton() {
}
public static StaticInnerSingleton getInstance() {
return SingletonHolder.mInstance;
}
}
- 这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。
- 不一样的是,它是在内部类里面去创建对象实例。
- 这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。
5. 枚举单例
public class EnumSingleton {
private EnumSingleton() {
}
public static EnumSingleton getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private EnumSingleton singleton;
//JVM会保证此方法绝对只调用一次
private Singleton() {
singleton = new EnumSingleton();
}
public EnumSingleton getInstance() {
return singleton;
}
}
}
- 在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法。
- 同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时(Singleton.INSTANCE),我们的单例被实例化。
- 也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
总结:
上面提到的五种种实现单例的方式都有共同的缺点:
- 需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
- 可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
单例模式的线程安全性:
- 饿汉式:线程安全
- 懒汉式:非线程安全
- 双检锁:线程安全
- 静态内部类:线程安全
- 枚举:线程安全
参考链接:
https://blog.csdn.net/fly910905/article/details/79286680
https://droidyue.com/blog/2017/07/17/singleton-in-kotlin/