设计模式-GOF-创建型-Singleton单例模式
定义
指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
主要解决: 一个全局使用的类频繁地创建与销毁
优点:
- 在内存中只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
- 避免对资源的多重占用
特点:
- 单例类只有一个实例对象
- 该单例对象必须由单例类自行创建
- 单例类对外提供一个访问该单例的全局访问点
使用场景
- 创建一个对象需要消耗的资源过多,比如I/O与数据库的连接
- 为控制实例数量,节省系统资源
- 全局数据共享
实现
- 私有化构造器,以防外部类来通过new生成实例
- 定义一个静态私有实例,并对外提供一个静态方法用于创建或获取静态私有实例
几种不同的实现方式
实现方式 | 是否线程安全 | 是否懒加载 | 是否防止反射构建 | 是否使用类加载机制 |
---|---|---|---|---|
饿汉式 | 是 | 否 | 否 | 是 |
双重校验锁 | 是 | 是 | 否 | 否 |
静态内部类 | 是 | 是 | 否 | 是 |
枚举 | 是 | 否 | 是 | ? |
枚举与类加载机制是否有关还没理解透
懒汉式,线程不安全
是否Lazy初始化:是
描述:实现最简单但不支持多线程。
public class SingleObject {
private static SingleObject instance;
private SingleObject() {}
public static SingleObject getInstance() {
if(instance==null) {
instance = new SingleObject();
}
return instance;
}
}
懒汉式,线程安全
是否Lazy初始化:是
描述:这种方式具备很好的Lazy loading,能保证多线程安全,但效率低,99%的情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费
缺点:加锁影响效率
public class SingleObject {
private static SingleObject instance;
private SingleObject() {}
public static synchronized SingleObject getInstance() {
if(instance==null) {
instance = new SingleObject();
}
return instance;
}
}
饿汉式(类加载时创建)
是否Lazy初始化:否
是否多线程安全:是
描述:比较常用,但容易产生垃圾对象
优点:没有锁,效率高
缺点:类加载时就初始化,浪费内存
它基于classloader机制避免了多线程同步问题,但没有达到lazy loading的效果
public class SingleObject {
private static SingleObject instance = new SingleObject();
private SingleObject() {}
public static SingleObject getInstance() {
return instance;
}
}
双检锁/双重校验锁(DCL,即double-checked locking)
JDK版本:JDK1.5起
是否Lazy初始化:是
是否多线程安全:是?
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。这种方法还有漏洞不能保证绝对的线程安全
public class SingleObject {
private static SingleObject instance;
private SingleObject() {}
public static SingleObject getInstance() {
if(instance==null) {
synchronized (SingleObject.class) {
if(instance==null) {
instance = new SingleObject();
}
}
}
return instance;
}
}
多线程不绝对安全的原因:
JVM会进行指令重排
创建对象有四步:
- 分配对象的内存空间
- 初始化对象
- 执行构造器初始化
- 将instance指向刚分配的内存地址(刚创建的对象)
经过优化顺序可能发生改变,可能导致A线程对象创建还没有完成,但B线程判断instance时instance已经不为null并且返回了一个没有初始化完成的instance对象。
改进,添加volatile
volatile 防止JVM进行指令重排
public class SingleObject {
private volatile static SingleObject instance;
private SingleObject() {}
public static SingleObject getInstance() {
if(instance==null) {
synchronized (SingleObject.class) {
if(instance==null) {
instance = new SingleObject();
}
}
}
return instance;
}
}
登记式/静态内部类
是否Lazy初始化:是
是否多线程安全:是
描述:对静态域使用延迟初始化。基于classloader类加载机制实现,在通过显式调用getInstance()时才会加载内部类,从而到达Lazy loading的目的。
注意:内部静态类无法从外部访问
public class SingleObject {
private static class LazyHolder{
private static final SingleObject INSTANCE = new SingleObject();
}
private SingleObject() {}
public static SingleObject getInstance() {
return LazyHolder.INSTANCE;
}
}
以上方法均可用反射打破单例
方法:
//获得构造器
Constructor con = SingleObject.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
SingleObject single1 = (SingleObject)con.newInstance();
SingleObject single2 = (SingleObject)con.newInstance();
//验证是否是不同对象
System.out.println(single1.equals(single2));
代码可以简单归纳为三个步骤:
第一步,获得单例类的构造器。
第二步,把构造器设置为可访问。
第三步,使用newInstance方法构造对象。
最后为了确认这两个对象是否真的是不同的对象,我们使用equals方法进行比较。毫无疑问,比较结果是false。
枚举
JDK版本:JDK1.5起
是否Lazy初始化:否
比较神奇简洁
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum SingleObject {
INSTANCE;
}
具体原理还需研究
备注:
- volatile关键字不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值。
- 使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。
- 对于其他方式实现的单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法。
private Object readResolve() throws ObjectStreamException{
return instance;
}
最后
经验之谈(这是我copy来的,具体还是看个人需求吧):一般情况下,不建议使用懒汉式线程不安全和懒汉式线程安全,建议使用饿汉式。只有在要明确实现 lazy loading 效果时,才会使用登记式/静态内部类。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双检锁/双重校验锁方式。
参考链接
https://mp.weixin.qq.com/s/2UYXNzgTCEZdEfuGIbcczA