🙊 前言:本文章为瑞_系列专栏之《23种设计模式》的单例模式篇,主要介绍单例模式的概念、结构、分类、实现方式、存在的问题以及单例模式的代码实现模版。由于博主是从菜鸟教程|设计模式以及黑马程序员Java设计模式详解学习设计模式的相关知识,所以文中的部分图和概念是出于它们。关于软件设计模式的概念、背景、优点、分类、以及UML图的基本知识和设计模式的6大法则,建议阅读 《瑞_23种设计模式_概述(含代码)》
??单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
??这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
??1??单例类只能有一个实例。
??2??单例类必须自己创建自己的唯一实例。
??3??单例类必须给所有其他对象提供这一实例。
意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决: 一个全局使用的类频繁地创建与销毁。
何时使用: 当您想控制实例数目,节省系统资源的时候。
如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
优点:
1??在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
2??避免对资源的多重占用(比如写文件操作)。
缺点:
1??没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
1??要求生产唯一序列号。
2??WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3??创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
瑞:如Spring IOC容器中的每个类对象默认情况下只有一个实例,就是单例模式的典型应用,再如JDK1.8中的Runtime类使用的是饿汉式(静态属性)方式来实现单例模式。由于单例模式的应用十分广泛,所以在面试中也是高频考点。
推荐使用双重锁、静态内部类、枚举方式的单例模式
??单例模式的主要有以下的两个角色:
????1??单例类:只能创建一个实例的类
????2??访问类:使用单例类
瑞:即单例类对外提供getInstance() 方法,该方法能获取全局唯一的自身实例对象,实例化过程由该类自己决定。外部访问类由于单例类本身的构造方法为私有即private修饰,所以要想使用单例类的实例对象只能通过单例类提供的getInstance()方法。
??单例模式主要分为饿汉式和懒汉式两种类型:
????1??饿汉式:类加载就会导致该单实例对象被创建。容易照成内存浪费
????2??懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
瑞:有很多初学的小伙伴很难分清这两种分类的区别,可以将懒汉式中的“懒”字,即英语“lazy”,与其特点延迟加载(lazy loading)也称为懒加载,进行绑定记忆。所以懒汉式可以理解记忆为延迟加载的单例,即当外部访问类调用单例类的getInstance() 方法的时候才会开始初始化单例对象。
??饿汉式单例在类加载的时候就初始化了,无论后续用不用都会进行初始化,非常容易造成浪费内存。所以饿汉式单例模式适用于单例对象较少的情况。
??饿汉式单例模式就像一家自助餐厅,勤奋的老板在开店前就把餐厅所有的菜准备好,有可能有些菜的制作过程极其复杂,老板需要花费大量的时间准备,这样就会导致开店的时间较晚;也有可能菜都准备好了之后,客人们当天并不想吃某些菜,导致了浪费;但对客人而言,客人不需要等待,能随时吃到自己想吃的菜,用户体验好。饿汉式由于在开店前就准备好了所有的单例菜,即类加载的时候就立即初始化创建单例对象,由于此时线程都还未出现,所以理论上饿汉式不存在线程安全的问题。
??Spring 框架中 IoC 容器 ApplocationContext 就是典型的饿汉式单例模式,ApplicationContext (默认)会自动装配(即非延迟加载,即非懒汉式,那就是饿汉式)Bean ,所以在创建容器对象的时候就对对象Bean进行初始化,并存储到一个容器中。当然Spring也支持修改配置以及加@Lazy注解等方式设置需要懒加载的bean,本文指的是其默认情况下使用的饿汉式。
??但要注意:
Spring的IoC容器使用的单例模式不是严格意义上的单例模式
,Spring的IoC容器设计主要是为了方便管理对象的生命周期和依赖关系,而不是限制Bean实例的数量。所以在IoC容器中是单例的,但在IoC容器之外,程序员仍然可以实例化创建Bean对象。而严格意义上的单例模式指的是整个应用程序中只存在该单例类唯一的实例对象
。我们学习设计模式主要是为了学习其代码设计经验的总结,让我们能在实际开发时能将其核心设计思维带到代码中,灵活使用这些思想
??这种方式比较容易实现,但非常容易产生垃圾对象。优点是没有加锁,执行效率会提高。缺点是类加载时就初始化,浪费内存。它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
/**
* 饿汉式 静态变量创建类的对象
*
* @author LiaoYuXing-Ray
* @version 1.0
* @createDate 2024/1/11 15:22
**/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 在成员位置创建该类的对象
private static Singleton instance = new Singleton();
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
??该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。
如果该对象足够大的话,而一直没有使用就会造成内存的浪费
。
??描述同饿汉式-方式1,和方式1相比主要是实现上有所不同,区别:对象的创建是在静态代码块中
/**
* 饿汉式 在静态代码块中创建该类对象
*
* @author LiaoYuXing-Ray
* @version 1.0
* @createDate 2024/1/11 15:22
**/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 在成员位置创建该类的对象
private static Singleton instance;
static {
instance = new Singleton();
}
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
??该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。
??这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,导致在实际工作中,也很少用。
/**
* 饿汉式 枚举
*
* @author LiaoYuXing-Ray
* @version 1.0
* @createDate 2024/1/11 15:22
**/
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
??
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次
,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式
。但不能通过 reflection attack 来调用私有构造方法。
??懒汉式单例的特点是延迟加载,即在第一次使用时才开始创建单例实例。可以有效避免不必要的对象创建,提高程序的性能。相比于饿汉式,懒汉式解决了内存浪费的问题,但同时又引出了线程安全的问题,即多线程环境下,容易出现线程安全的问题。
??懒汉式的名字来源于它的特点,懒汉式单例模式就像一个懒惰的厨师,只有在客人点餐时才会开始做菜。这样虽然节省了厨师的准备时间(厨师只要为这个客人点的菜进行准备,不需要将餐厅所有的菜都准备好再上菜),但有时候可能因为某些菜的制作过程十分繁琐,会导致客人等得着急,甚至可能有两个及以上的客人同时点同样的菜,导致厨师手忙脚乱做出了两份相同的菜(可能比喻不恰当,但此处设定是单例即不能出现两份相同的菜也就是该单例类的两个对象,此处主要是想表达懒汉式可能存在线程安全的问题)
??以下是懒汉式最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
/**
* 懒汉式 线程不安全
*
* @author LiaoYuXing-Ray
* @version 1.0
* @createDate 2024/1/11 15:22
**/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 在成员位置创建该类的对象
private static Singleton instance;
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
??从上面代码我们可以看出该方式在成员位置声明
Singleton
类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?是当调用getInstance()
方法获取Singleton
类的对象的时候才创建Singleton
类的对象,这样就实现了懒加载的效果。但是在多线程环境下,会出现线程安全问题
。
??这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,因为 99% 情况下不需要锁,只有第一次调用的时候才需要。优点是第一次调用才初始化,避免内存浪费。缺点是必须加锁 synchronized 才能保证单例,但加锁会影响效率。
/**
* 懒汉式 线程安全
*
* @author LiaoYuXing-Ray
* @version 1.0
* @createDate 2024/1/11 15:22
**/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 在成员位置创建该类的对象
private static Singleton instance;
// 对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
??该方式实现了懒加载效果,同时又解决了线程安全问题。但是在
getInstance()
方法上添加了synchronized
关键字,导致该方法的执行效果特别低。而且从上面代码我们可以看出,其实就是在初始化instance
的时候才有可能会出现线程安全问题,一旦初始化完就不存在线程安全的问题了,导致绝大多数情况下的性能是浪费了
??双检锁/双重校验锁(DCL,即 double-checked locking),这种方式采用双锁机制,安全且在多线程情况下能保持高性能。因为 getInstance() 的性能对应用程序很关键。
??我们思考懒汉式-方式2的方法中加锁的问题,对于 getInstance()
方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们只需要调整加锁的时机。即产生了一种新的懒汉式实现模式:双重校验锁模式。即两次INSTANCE
判空。
/**
* 懒汉式 双重校验锁方式
*
* @author LiaoYuXing-Ray
* @version 1.0
* @createDate 2024/1/11 15:22
**/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 注意此处没有加 volatile 。无法防止指令重排序,就有可能先赋值再调用构造方法,对(synchronized)外面的线程而言,拿到的引用还是未调用构造方法的,就出错了
private static Singleton instance;
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
// 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null) {
synchronized (Singleton.class) {
// 抢到锁之后再次判断是否为null
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
??双重校验锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重校验锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是:通过字节码分析是可以得出赋值和初始化是可以被重排序的,就有可能先赋值再调用构造方法,对(synchronized)外面的线程而言,拿到的引用还是未调用构造方法的,就出错了,因为JVM在实例化对象的时候会进行优化和指令重排序操作
??要解决双重检查锁模式带来空指针异常的问题,只需要使用
volatile
关键字,volatile
关键字可以保证可见性和有序性,避免了一个线程只是把引用指向了堆空间地址但还没初始化完对象, 而另一个线程已经判断不为null而获取到了没有初始化完成的不完整对象
/**
* 懒汉式单例 - 双重检查锁
*
* @author LiaoYuXing-Ray
* @version 1.0
* @createDate 2024/1/11 15:22
**/
public class Singleton {
/**
* 私有构造方法
*/
private Singleton() {}
/*
volatile 关键字防止指令重排序
*/
private volatile static Singleton INSTANCE;
/**
* 对外提供静态方法获取该对象
**/
public static Singleton getInstance() {
// 第一次判空,如果instance不为null,不进入抢锁阶段,减少锁的竞争
if (INSTANCE == null) {
// 加锁使得多线程访问下安全
synchronized (Singleton.class) {
// 抢到锁之后,第二次判空确保单个实例
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
小结:
??添加volatile
关键字之后的双重校验锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题
??登记式/静态内部类:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。但这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用
。
??这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟饿汉式-方式1不同的是:饿汉式-方式1只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 LazyHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 LazyHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比饿汉式-方式1就显得很合理。
/**
* 懒汉式单例 - 静态内部类
*
* @author LiaoYuXing-Ray
* @version 1.0
* @createDate 2024/1/11 15:22
**/
public final class Singleton {
// 私有构造方法
private Singleton() {}
// 静态内部类
private static class LazyHolder{
private static final Singleton INSTANCE = new Singleton();
}
// 对外提供静态方法获取该对象
public static final Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
??静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被
static
修饰,保证只被实例化一次,并且严格保证实例化顺序
??注意:第一次加载Singleton
类时不会去初始化INSTANCE
,只有第一次调用getInstance
,虚拟机才加载LazyHolder
,并初始化INSTANCE
,这样不仅能确保线程安全,也能保证Singleton
类的唯一性
??小结:静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
??破坏单例模式:使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射
??注意:枚举方式不会出现这两个问题
??通过序列化反序列化破坏单例模式,代码如下:
??单例类:
import java.io.Serializable;
public class Singleton implements Serializable {
// 私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
??测试类:
import java.io.*;
/**
* 通过序列化反序列化破坏单例模式
*
* @author LiaoYuXing-Ray
* @version 1.0
* @createDate 2024/1/11 15:22
**/
public class RayTest {
public static void main(String[] args) throws Exception {
// 获取当前文件的绝对路径
String absolutePath = System.getProperty("user.dir");
String filePath = absolutePath + File.separator + "test.txt";
// 往文件中写对象
writeObjectFile(filePath);
// 从文件中读取对象
Singleton s1 = readObjectFromFile(filePath);
Singleton s2 = readObjectFromFile(filePath);
// 判断两个反序列化后的对象是否是同一个对象
System.out.println(s1 == s2);
}
private static Singleton readObjectFromFile(String filePath) throws Exception {
// 创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
// 第一个读取Singleton对象
Singleton instance = (Singleton) ois.readObject();
return instance;
}
public static void writeObjectFile(String filePath) throws Exception {
// 获取Singleton类的对象
Singleton instance = Singleton.getInstance();
// 创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
// 将instance对象写出到文件中
oos.writeObject(instance);
}
}
上面代码运行结果是
false
,表明序列化和反序列化已经破坏了该单例设计模式
??通过反射破坏单例模式,代码如下:
??单例类:
public class Singleton {
// 私有构造方法
private Singleton() {}
private static volatile Singleton instance;
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}
??测试类:
import java.lang.reflect.Constructor;
/**
* 通过反射破坏单例模式
*
* @author LiaoYuXing-Ray
* @version 1.0
* @createDate 2024/1/11 16:58
**/
public class RayTest {
public static void main(String[] args) throws Exception {
// 获取Singleton类的字节码对象
Class<?> clazz = Singleton.class;
// 获取Singleton类的私有无参构造方法对象
Constructor<?> constructor = clazz.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
// 创建Singleton类的对象s1
Singleton s1 = (Singleton) constructor.newInstance();
// 创建Singleton类的对象s2
Singleton s2 = (Singleton) constructor.newInstance();
// 判断通过反射创建的两个Singleton对象是否是同一个对象
System.out.println(s1 == s2);
}
}
??上面代码运行结果是
false
,表明反射已经破坏了单例设计模式
??序列化方式破解单例的解决方法:在Singleton类中添加readResolve()
方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。
??代码如下:
public class Singleton implements Serializable {
// 私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
??为什么这样能解决序列化破坏单例,可以通过源码解析——ObjectInputStream类
public final Object readObject() throws IOException, ClassNotFoundException{
...
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);// 重点查看readObject0方法
.....
}
private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));// 重点查看readOrdinaryObject方法
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
// isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
// 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
Object rep = desc.invokeReadResolve(obj);
...
}
return obj;
}
??反射方式破解单例的解决方法:当通过反射方式调用构造方法进行创建创建时,直接抛异常,以此阻止操作。核心代码如下所示:
// 私有构造方法
private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
??完整示例代码如下:
public class Singleton {
// 私有构造方法
private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
private static volatile Singleton instance;
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}
??由于存在种种已知原因,以懒汉式-方式3(DCL)为例,比较好的单例的模版如下:
/**
* 懒汉式单例 - 双重校验锁(DCL)
* <p>私有构造内防止反射破解单例</p>
* <p>添加readResolve方法解决序列化反序列化破解单例模式</p>
*
* @author lyx-Ray
* @author LiaoYuXing-Ray
* @version 1.0
* @createDate 2024/1/11 17:10
**/
public class GoodSingleton {
/**
* 私有构造方法
*/
private GoodSingleton() {
/*
防止反射破解单例模式
*/
if (INSTANCE != null) {
throw new RuntimeException();
}
}
/*
volatile关键字防止指令重排序
JVM在实例化对象的时候会进行优化和指令重排序操作
避免了一个线程只是把引用指向了堆空间地址但还没初始化完对象
而另一个线程已经判断不为null而获取到了没有初始化完成的不完整对象
*/
private volatile static GoodSingleton INSTANCE;
/**
* 对外提供静态方法获取该对象
**/
public static GoodSingleton getInstance() {
// 第一次判空,如果instance不为null,不进入抢锁阶段,减少锁的竞争
if (INSTANCE == null) {
// 加锁使得多线程访问下安全
synchronized (GoodSingleton.class) {
// 抢到锁之后,第二次判空确保单个实例
if (INSTANCE == null) {
INSTANCE = new GoodSingleton();
}
}
}
return INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return GoodSingleton.INSTANCE;
}
}
??并非线程不安全就不好,也并非懒加载就好,每种实现方式都有其实际的应用场景,记住:没有对错,只有在某场景下适合与更适合。
??一般情况下,不建议使用懒汉式-方式1(线程不安全)和懒汉式-方式2(线程安全),建议使用饿汉式-方式1(静态变量)和饿汉式-方式2(静态代码块)。只有在要明确实现 lazy loading 效果时,才会使用懒汉式-方式4(静态累内部类),但又由于一般工作上要求的都是懒加载,所以工作时常用。如果涉及到反序列化创建对象时,可以尝试使用饿汉式-方式3(枚举)。如果有其他特殊的需求,可以考虑使用懒汉式-方式3(DCL)
??如果觉得这篇文章对您有所帮助的话,请动动小手点波关注💗,你的点赞👍收藏??转发🔗评论📝都是对博主最好的支持~