单例模式
不使用单例模式会出现什么问题?
在使用单例模式之前,如果程序中需要某个对象,就直接new一个出来,即每次使用都要创建一个新的对象。
这种方法在某些场景下会出现明显的问题,有些对象创建成本很高:
数据库连接池
线程池
缓存管理器
配置中心客户端
日志管理器
这些对象一般不需要创建很多份。
其次多个对象会导致状态不一致:
系统本来希望共享一份配置,
但因为创建了多个对象,
导致状态分裂。
最后是无法限制对象的数量,有些类从业务语义上就应该只有一个对象:
系统配置管理器
任务调度器
全局 ID 生成器
Spring 容器
日志管理器
如果随便的new,就没有办法约束外部代码
单例模式解决的问题
单例模式主要解决两个问题:
- 保证一个类只有一个实例
- 提供一个全局访问点
单例模式的核心就是保证一个类在程序运行过程中只有一个实例,并提供一个全局访问点来获取这个实例。
通常实现单例模式要有三个核心设计点:
1. 构造方法私有化,防止外部 new 对象
2. 类内部自己创建唯一实例
3. 对外提供静态方法返回这个实例
单例模式的饿汉式实现
public final class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
这是一个典型的饿汉式的单例模式。
优点:
- 线程安全:在类加载阶段中的初始化阶段就完成了实例化,类加载过程天然线程安全,避免了多线程同步。
- 获取速度快:实例已经加载完成,可以直接返回
- 防止反射攻击
缺点:
- 内存浪费:无论是否使用,它都会创建
- 启动延迟:如果单例的初始化很耗时,会延迟应用的启动时间
因此饿汉式单例模式适用于:
- 对象创建成本不高
- 对象一定会被使用
单例模式的懒汉式实现
public final class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒加载的思想是:最初并不需要实例化instance,⽽是当getInstance()⽅法被第⼀次调⽤时,创建单例对象。但这会引出线程同步的问题,因此需要使用synchronized来锁住同步。
但是写操作只会在还没有实例化instance的情况下发生,其余都是读操作,这样直接用synchronized将整个方法包裹,太过于笨重,性能不够好。
单例模式的懒汉式双重检查实现
public final class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
第一次判断避免读操作也加锁,第二次判断防止排队的线程获取锁后再次创建新的实例。
volatile关键字防止指令重排序导致拿到还没有初始化的对象,其他业务代码直接使用这个没有初始化的对象:
线程 A:
进入 synchronized
分配对象内存
instance = memory // 此时 instance 已经不是 null
对象还没初始化完
线程 B:
执行第一次 if (instance == null)
发现 instance != null
直接 return instance
使用了一个未初始化完成的对象
同时volatile也防止在instance = new Singleton()后续线程B就能直接看到最新的值,不需要等待再加锁才能看到最新值。
单例模式的静态内部类懒加载实现
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // 这里才会触发类加载,实现懒加载
}
}
静态内部类是一个独立的类,它与外部类没有绑定关系。静态内部类和其外部类在编译后会生成两个独立的字节码文件(.class文件)这意味着:
- 不会随外部类加载而加载:加载
Singleton类时,不会自动加载Holder类 - 有自己的类加载时机:
Holder类只在被引用时才加载
因此上述的单例模式,只有在调用Holder.INSTANCE时才会创建实例,其线程安全有JVM进行保证:
当多个线程同时首次调用getInstance()时:
- 第一个线程触发
Holder类的加载 JVM的类加载机制保证<clinit>(类初始化方法)只会被执行一次- 其他线程会等待类加载完成
- 所有线程都得到同一个已初始化的实例
单例模式的枚举类实现
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("执行逻辑");
}
}
枚举单例的优点:
- 写法简单
- 线程安全
- 天然防止反射破坏
- 天然支持序列化
普通单例的私有构造方法可以通过反射强行调用:
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s = constructor.newInstance();
但枚举类的实例创建由JVM控制,反射不能随便创建枚举对象。
所以枚举单例在安全性上很强。