Singleton单例模式
why
控制实例数目,节省系统资源
what
保证一个类只有一个实例,并提供一个访问它的全局访问点
how
判断是否已经有这个单例,如果有则返回,如果没有则创建。
饿汉式
public class Singleton {
// 定义变量时直接初始化
private static Singleton instance = new Singleton();
// 构造方法私有,这样该类就不会被实例化,不允许外部new
private Singleton() {
}
// 外部通过getIntstance获取实例
public static Singleton getInstance() {
return instance;
}
}
饿汉式在定义instance时直接实例化。通过饿汉式完全可以保证实例对象的线程安全。
但是有一个问题,如果该实例对象被创建之后过了很久才会被访问,那么在访问之前这个对象数据会一直存放在堆内存当中,如果实际场景中单例对象的实例数据很大,将会占用较多资源,这种方式则不太合适。
懒汉式
实际开发中不可以使用
public class Singleton {
// 定义变量时不做初始化
private static Singleton instance (= null);
// 构造方法私有,不允许外部new
private Singleton() {
}
// 外部通过getIntstance获取实例
public static Singleton getInstance() {
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
相对饿汉式,可以避免空间资源浪费。
但是在多线程情况下,调用getInstance()会有线程安全问题。getInstance()不是原子操作。
懒汉式+同步方法
public class Singleton {
// 定义变量时不做初始化
private static Singleton instance;
// 构造方法私有,不允许外部new
private Singleton() {
}
// 方法上加synchronized保证线程安全
public static synchronized Singleton getInstance() {
if(instance==null){
instance = new Singleton();
}
return instance;
}
}
但在实例被成功创建之后,每次获取实例时都需要获取锁,这会极大降低性能。
双重检查锁
(DCL,即 double-checked locking)
public class Singleton {
private static Singleton instance;
String msg;
private Singleton() {
// 初始化msg
this.msg = "对象描述信息";
}
public static Singleton getInstance() {
// 第一层检查
if (instance == null) {
// 只能有一个线程获得Singleton.class锁
synchronized (Singleton.class) {
// 第二层检查
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
DCL方式保证线程安全没有偷懒,没有直接在方法上加锁。
第一层如果true,则会执行加锁和创建实例的操作;如果第一层false,表示对象已经创建,那么直接返回实例,避免每次请求都加锁,性能高。
加锁后的第二层的null检查是为了防止在进入第一层检查和加锁成功的过程中已经有其他线程完成实例的创建,避免重复创建。
但是这种方式在多线程情况下可能会出现空指针异常。Singleton()构造方法中对属性msg进行了初始化赋值。
在getInstance()方法中的instance = new Singleton();可以分为三步操作。
- 1.new Singleton()在堆中创建一个对象;
- 2.msg赋值;
- 3.instance赋值;
根据JVM运行时Happens-before规则[1],这三步的顺序并没有前后依赖,很有可能实际运行的顺序是,3->1->2或者其他;
那么就可能造成在getInstance()中instance == null的结果为true,但是实例中的msg为null;
假设调用方拿到instance之后直接使用msg则会抛出空指针异常。
所以可以用volatile关键字防止重排序的发生。
volatile+DCL
public final class Singleton {
// volatile修饰
private static volatile Singleton instance;
静态内部类
主要是借助于类加载机制完成。
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
// 实际是返回静态内部类中的实例
return Holder.instance;
}
private static class Holder {
// 在静态内部类中定义instance并实例化
private static Singleton instance = new Singleton();
}
}
静态内部类并不会随着外部类的加载一起加载,只有在使用时才会加载;
而类加载的过程则直接保证了线程安全性,保证实例对象的唯一。
这种方式又被称为IoDH(Initialization Demand Holder)技术,是目前使用比较广的方式之一,也算是最好的一种单例设计模式。
枚举
public enum Singleton {
INSTANCE;
String data = "实例数据";
Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
枚举不可以被继承,并且线程安全,只会实例化一次。
打破单例
反射处理私有化构造方法可以突破这个限制。
《Effective Java》中推荐枚举。因为jdk处理过构造方法。 枚举类型 通过反射执行时并不能拿到。
P&Cs Pros and cons
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。避免对资源的多重占用。
缺点:
- 可以理解为一个全局变量,没有接口,不能继承。违背了SRP单一职责原则。一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
[1]happens-before先行发生原则
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,这两个操作之间必须存在happens-before关系,它是可见性与有序性的一套规则总结,即先行发生原则。
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见(可见性),而且第一个操作的执行顺序排在第二个操作之前(有序性)
A happens-before B,意味着A发生的事情对B来说是可见的,无论A和B是否发生在同一个线程里 - 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法(可以指令重排)↩(返回原文)
参考
掘金 https://juejin.cn/post/7012557664332808222
知乎 https://zhuanlan.zhihu.com/p/150004430