单例模式是设计模式中最常用、最基础的设计模式之一,几乎在所有的项目中都会用到。虽然单例模式很简单,但是想要学好它也是需要花一定的功夫,尤其是某些细节方面需要小心使用。这篇文章将从概念到源码来详细讲解单例模式,从而吃透单例模式。
一、基本概念
1,定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
简单的说,在编码中,让某个类在整个程序中只能new出一个对象,不管在哪调用、什么时候调用,获取的都是同一个实例。
就比如说,操作系统里面的“任务管理器”,不管我打开多少次,显示的都是同一个窗口。
2,目的和适用场景
核心目的
避免资源浪费。有些对象创建过程复杂,反复创建开销很大。单例模式让对象创建一次,避免了反复创建性能开销。
确保状态一致。在某些类的实例对象需要存储全局数据时(比如配置、工具等等),多个实例会导致参数混乱。单例模式可以保证全程序参数信息一致。
适用场景
数据库连接池、线程池、客户端API、系统配置类,日志工具类等等。
3,核心设计思想
单例模式的核心是“控制对象创建”和“提供全局访问”。
控制对象创建:封闭对外的构造方法,禁止外部创建对象,转由自己内部实现对象的创建。
提供全局访问:提供一个开放的静态访问入口,确保始终拿到的是同一个对象。
单例模式符合“单一职责原则”,因为单例类只负责创建自身唯一的实例,且提供了唯一的访问点。不提供多余的业务逻辑。
单例模式的构造要点:
1,构造方法私有化。目的是不能在其他地方实例化该对象。
2,实例唯一。内部仅维护同一个实例,必须通过某些手段确保实例唯一。
3,全局访问点。提供一个public的静态方法访问该实例。
4,单例模式中的角色
单例中仅有一个单例类(Singleton)角色。
5,类图
就是这么简单的一个结构。
二、常见写法(源码 + 优缺点分析)
单例模式的写法有很多种,我这里提供常见的4种写法:饿汉式、懒汉式、枚举、静态内部类。并分析各自的优缺点。
1,饿汉式
public class Singleton {
//饿汉式,在静态成员变量直接初始化
private static final Singleton singleton = new Singleton();
//私有化构造方法
private Singleton(){}
//提供一个开放的全局访问点
public static Singleton getInstance(){
return singleton;
}
}
优点:简单,在程序启动后立即实例化。由JVM保证线程安全。
缺点:程序启动后就初始化,一直就占用内存,如果一直不使用,浪费了内存。
适用场景:单例实例较少的系统,或者程序一启动就需要使用的场景。
2,懒汉式
懒汉式,是将对象的创建过程延迟到使用之前。但是在多线程并发环境下,会出现线程安全问题。可以使用DCL(双重检查锁)来解决。
public class Singleton {
//2,静态属性通过volatile修饰,保证多线程下的可见性,防止指令重排序
private static volatile Singleton singleton;
//1,私有化构造方法
private Singleton(){}
public static Singleton getInstance(){
//1,第一次检查,如果不为空,则直接返回,避免每次加锁,提高性能
if(singleton == null){
//2,加锁,保证同一时间只有一个线程进入创建对象
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
为什么要用volatile
使用volatile关键字,是为了防止指令重排序。因为对象创建过程分为3步:
1,分配内存。
2,初始化。
3,引用指向内存。
JVM在执行时,可能会对上面三个指令重排序(形成1->3 ->2),会导致对象已经创建,instance引用不为空,但是还没有实例化。形成半成品状态。
使用volatile关键字,可以保证对象创建的三个过程不会被指令重排序,从而避免了线程获取到中间状态的对象。
为什么需要使用DCL(双重检查锁)
假设不使用DCL,没有第二个if判断,我们来分析看看过程。
1,假设两个线程A、B进入到getInstance方法,执行外层的if判断。
2,接下来如果A线程获取锁,那么A线程会创建一个对象,然后释放锁。
3,接下来线程B获取锁,线程B继续创建对象。最终导致存在两个对象。
因此,需要在synchronized同步代码块中,继续检查一次。
如果是没有第一个if判断,虽然能解决多个实例情况。但是因每次都synchronized加锁,性能低,外层加一个if判断,为了提高性能。
优点:仅在第一次使用时创建(按需创建),节省了内存占用。
缺点:代码复杂了。需要配合volatile、synchronized确保线程安全。
适用于大多数场景,是单例模式的普遍写法。
3,静态内部类
静态内部类写法,是利用“静态内部类只有在使用时才会被加载”的特性,来实现“按需加载”,也能保证线程安全。
这个写法,也是我经常在代码中写的一种方法,看起来比较高级,没有if-else,也不需要加锁。
public class SingletonInnerClass {
//1,私有化构造方法
private SingletonInnerClass(){}
//提供对外访问入口
public static SingletonInnerClass getInstance(){
return SingletonInner.instance;
}
//静态内部类
private static class SingletonInner{
private static SingletonInnerClass instance = new SingletonInnerClass();
}
}
优点:按需创建、JVM天然保证线程安全。
缺点:反射下,仍然能够创建多个对象。
4,枚举
public enum SingletonMenu {
INSTANCE;
public void method() {
System.out.println("枚举单例模式");
}
//使用方法
public static void main(String[] args) {
SingletonMenu.INSTANCE.method();
}
}
优点:线程安全(JVM保证枚举唯一)、序列化情况下也能保证唯一、反射也不会创建新实例、代码及其简单。
缺点:类初始化时就创建实例,没法做到“按需创建”。
适用于对安全性极高要求的场景。
三、源码中应用
1,spring中的Bean管理
Spring容器管理bean,默认情况下都是单例,其核心是通过DefaultSingletonBeanRegistry实现,相关代码如下:
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
//第一次非null检查
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
//第二次非null检查
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
其中可以看出,Spring中的单例模式,是用的DCL双重检查锁的写法。
2,JDK中的java.lang.Runtime
再来看Runtime类:
public class Runtime {
//静态属性
private static Runtime currentRuntime = new Runtime();
//提供对外的全局访问点
public static Runtime getRuntime() {
return currentRuntime;
}
//构造方法私有化
private Runtime() {}
很熟悉吧,这是饿汉式的单例写法.
四、优缺点总结、避坑
1,总结
优点:
节省资源,避免重复创建对象,检查内存/CPU消耗。
保证状态一致,全程序使用同一个实例。
缺点:
扩展困难,构造方法私有化了,无法扩展。
2,避坑指南
2.1序列化、反序列化破坏单例
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
oos.writeObject(Singleton.getInstance());
// 反序列化:会生成新实例
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.txt"));
Singleton newInstance = (Singleton) ois.readObject();
System.out.println(Singleton.getInstance() == newInstance);
上面的示例代码,会输出false。表示单例模式下创建两个对象。
解决方法:在单例模式下增加readResolve()方法:
private Object readResolve() {
return singleton;
}
2.2反射破坏单例
原因:反射可以访问私有构造方法。
看如下示例代码:
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton1 = constructor.newInstance();
System.out.println(singleton1 == Singleton.getInstance());
该代码返回false,说明有两个对象了。
解决方法:在狗杂方法中添加非null判断,并抛出异常。或者使用枚举,因为反射无法创建枚举实例。
if(null != singleton){
throw new RuntimeException("单例模式不允许多个实例");
}
五、与其他模式搭配
1,单例 + 工厂
工厂模式是负责创建对象的地方,一般情况下创建对象的地方是不怎么变化的,因此可以将工厂构造成单例模式。
2,单例 + 建造者
建造者模式是将负责对象创建过程分步化,通过一系列分步操作,创建负责的对象。实际开发中一般也是将建造中构建成单例。
六、与相似设计模式对比区别
单例模式容易与“工厂模式”、“享元模式”混淆,接下来就分析其区别。
1,单例与工厂模式
对比维度 | 单例 | 工厂 |
---|---|---|
核心目标 | 保证一个类只有一个实例 | 解耦对象创建与使用,分离出对象创建过程 |
实例数量 | 1 | 多个 |
角色数量 | 1 | 至少2个(工厂类、产品类) |
一句话总结:单例是控制实例数量,工厂是控制实例的创建。
2,单例与享元模式
对比维度 | 单例 | 享元 |
---|---|---|
核心目标 | 一个类只有一个实例 | 复用多个相似实例,减少内存 |
实例数量 | 1 | 多个 |
状态管理 | 有 | 分内部状态(共享)、外部状态(不共享) |
适用场景 | 全局唯一对象(工具类) | 大量相似对象 |