一、概述
1、简介
单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点供外部获取该实例,通常用于控制资源的唯一性,比如配置管理器、日志对象或是数据库连接等,这样可以避免多例造成的资源浪费和潜在的数据不一致问题。
单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。------《大话设计模式》
2、 分类
-
饿汉式:类加载就会创建单实例对象
-
懒汉式:类加载不会创建单实例对象,而是首次使用该对象时被创建
二、实现
1、饿汉式
(1)静态变量方式
通过将对象的实例设置为静态的方式,保证了该对象的实例,永远只有一份,且该对象的创建在类加载时就会立即创建在 JVM 内存中的方法区,在程序运行期间永久存在,所以当对象太大的时候就会造成一种资源的浪费。
java
/**
* @Description: Hungry man style 饿汉式 方式 1 静态变量方式
* @Author: QiuXuan
**/
public class HMS_01 {
// 私有构造方法
private HMS_01() {}
// 在成员位置创建该类的对象
private static final HMS_01 instance = new HMS_01();
// 对外提供公共的访问方法
public static HMS_01 getInstance() {
return instance;
}
}
(2)静态代码块方式
对象的创建是在静态代码块中,也是在类的加载时创建。
java
/**
* @Description: Hungry man style 饿汉式 方式 2 静态代码块方式
* @Author: QiuXuan
**/
public class HMS_02 {
// 私有构造方法
private HMS_02() {}
// 在成员位置创建该类的对象
private static final HMS_02 instance;
static {
instance = new HMS_02();
}
// 提供公共的访问方法
public static HMS_02 getInstance() {
return instance;
}
}
(3)枚举
在 Java 中,枚举(enum
)是一种数据类型,用于定义一组固定的命名常量,常用于表示一系列预定义的选项或状态,具有类型安全性和内置的方法支持。
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,充分利用枚举的这个特性来实现单例模式
枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
java
/**
* @Description: Hungry man style 饿汉式 方式 3 枚举
* @Author: QiuXuan
**/
public enum HMS_03 {
INSTANCE;
}
2、懒汉式
饿汉就是一直处于饿的状态,需要不断有食物给你,也就是对象一直存在。
而懒汉式,就比较懒惰,只有真正饿的时候才会寻找食物,也就是请求对象实例。
(1)线程不安全
只有调用 getInstance() 方法获取类的对象的时才创建对象,就实现了懒加载。
但是,如果是多线程环境下,每个线程抢占对象资源,但是可能会发生对个线程同时请求对象实例的问题,这个时候就有可能创建多个对象,从而导致数据不一致,就会出现线程安全问题。
java
/**
* @Description: Lazy Chinese style 懒汉式 方式 1 线程不安全
* @Author: QiuXuan
**/
public class LCS_01 {
// 私有构造方法
private LCS_01() {}
// 在成员位置创建该类的对象
private static LCS_01 instance;
// 对外提供公共的访问方法
public static LCS_01 getInstance() {
if (instance == null) {
instance = new LCS_01();
}
return instance;
}
}
(2)线程安全
针对方式 1 的线程不安全,通过加同步锁的机制,保证了每次只有一个线程可以操作当前的对象,即可确保线程安全。
但是由于加锁就会导致该代码执行效率特别低,从上面代码可以看出,其实就是在初始化 instance 的时候才会出现线程安全问题,一旦初始化完成就不存在了,所以可以做出进一步优化。
java
/**
* @Description: Lazy Chinese style 懒汉式 方式 2 线程安全
* @Author: QiuXuan
**/
public class LCS_02 {
// 私有构造方法
private LCS_02() {}
// 在成员位置创建该类的对象
private static LCS_02 instance;
// 对外提供公共的访问方法
public static synchronized LCS_02 getInstance() {
if (instance == null) {
instance = new LCS_02();
}
return instance;
}
}
(3)双重检查锁
对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以没必要让每个线程必须持有锁才能调用该方法,需要调整加锁的时机,由此也产生了一种新的实现模式:双重检查锁模式
java
/**
* @Description: Lazy Chinese style 懒汉式 方式 3 双重检查锁
* @Author: QiuXuan
**/
public class LCS_03 {
// 私有构造方法
private LCS_03() {}
// 在成员位置创建该类的对象
private static LCS_03 instance;
// 对外提供公共的访问方法
public static LCS_03 getInstance() {
// 第一次判断,如果 instance 不为空,不进入抢锁阶段,直接返回实例
if (instance == null) {
synchronized (LCS_03.class){
// 第二次判断,抢锁成功后再次判断 instance 是否为空
if (instance == null)
instance = new LCS_03();
}
}
return instance;
}
}
在双重检查锁模式下,为什么要进行两次的判断呢?
现在假设有两个线程 a、b,两个线程都去请求单例模式下类的实例,当第一个判断的时候,两个线程都会进入判断代码块中进行锁的抢占,最终 a 抢占到了锁,那么 b 只能在加锁的代码块外部进行等候,这个时候 a 创建了对象的实例,完成功能后归还了锁,这个时候线程 b 马上抢占到了锁,然后进入内部代码块,假设在这里没有第二次判断的话,线程 b 就会再次创建一个新的对象,所以,要在这里再加一次判断。
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,但是呢,JVM 在实例化对象的时候会进行优化和指令重排序操作,在多线程的情况下,就可能会出现空指针问题。
解决空指针问题只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性,这个关键字禁止了对当前修饰的变量上下文重排序,保证了方法的可靠性。
java
/**
* @Description: Lazy Chinese style 懒汉式 方式 3 双重检查锁
* @Author: QiuXuan
**/
public class LCS_03 {
// 私有构造方法
private LCS_03() {}
// 在成员位置创建该类的对象
// 使用 volatile 修饰,禁止重排序
private static volatile LCS_03 instance;
// 对外提供公共的访问方法
public static LCS_03 getInstance() {
// 第一次判断,如果 instance 不为空,不进入抢锁阶段,直接返回实例
if (instance == null) {
synchronized (LCS_03.class){
// 第二次判断,抢锁成功后再次判断 instance 是否为空
if (instance == null)
instance = new LCS_03();
}
}
return instance;
}
}
(4)静态内部类方式
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。而静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。
java
/**
* @Description: Lazy Chinese style 懒汉式 方式 4 静态内部类方式
* @Author: QiuXuan
**/
public class LCS_04 {
// 私有构造方法
private LCS_04() {}
private static class LazySingletonHolder {
private static final LCS_04 INSTANCE = new LCS_04();
}
// 对外提供公共的访问方法
public static LCS_04 getInstance() {
return LazySingletonHolder.INSTANCE;
}
}
第一次加载类时不会去初始化 INSTANCE,只有第一次调用 getInstance(),虚拟机加载 LazySingletonHolder(),并初始化 INSTANCE,这样不仅能确保线程安全,也能保证唯一性。
所以静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。
在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
三、缺陷
在上面定义的单例类中正常的使用的情况下只可以同时只有一个对象存在,但是存在着一些操作可以破坏这种现象,使得单例模式可以创建多个对象(枚举方式除外)
1、序列化反序列化
对于序列化与反序列化破坏单例模式的问题,是通过 readObject()
方法出现了破坏单例模式的现象,主要是因为这个方法最后会通过反射调用无参数的构造方法创建一个新的对象,从而每次返回的对象都不一致。
解决方案:使用 readResolve()
方法
readResolve()
是一个特殊的方法,当一个对象被反序列化时会被自动调用。这个方法应该返回一个对象,这个对象将会被用作反序列化的结果。这对于单例特别有用,因为它允许你在反序列化时返回已存在的单例实例而不是创建一个新的实例。
当一个对象被反序列化时,首先检查该类是否有 readResolve()
方法(通过 hasReadResolveMethod
),如果有,那么它会调用这个方法(通过 invokeReadResolve
)
为了使单例类在反序列化时也保持单例特性,应该在类中实现 readResolve()
方法,并返回单例实例。
2、反射
对于反射破坏单例模式是因为单例模式通过 setAccessible(true)
指示反射的对象在使用时,取消了 Java 语言访问检查,使得私有的构造函数能够被访问,而单例模式的设计在于只保留一个公有静态函数来获取唯一的实例,其他方法(构造函数)或字段为私有,外界不能访问。反射破坏了这一原则,它突破了构造函数私有的限制,可以获取单例类的私有构造函数并使用其创建多个对象。
反射是一种暴力获取对象实例的方法,因为他可以直接访问 private 修饰的构造函数,所以在对于反射方式破坏单例模式的问题上只能采取被动的防御,
既然能访问构造函数,那就在构造函数中建立防御机制,不允许通过构造函数创建多个实例对象。
java
/**
* @Description: 防止破坏单例模式
* @Author: QiuXuan
**/
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 防止反射攻击
if (INSTANCE != null) {
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public static Singleton getInstance() {
return INSTANCE;
}
// readResolve 方法用于反序列化时返回正确的单例实例
protected Object readResolve() {
return getInstance(); // 返回已存在的单例实例
}
}
四、应用
1、使用原因
单例模式的主要目的是确保某个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取该实例。使用单例模式的原因包括:
1)资源管理:减少资源消耗,特别是对于那些创建成本高且生命周期长的对象,如数据库连接、线程池等。
2)全局访问:提供一个全局访问点,方便在应用程序的任何位置访问同一个对象,例如配置管理器、日志记录器等。
3)简化实现:通过限制实例的数量来简化对象的管理,避免多例造成的复杂性和潜在的数据不一致性问题。
4)线程安全:单例模式天然适合于线程安全的场景,因为实例的创建和访问通常是同步的,避免了多线程环境下并发访问的问题。
2、应用场景
单例模式适用于以下场景:
1)配置文件管理:应用中需要频繁读取配置文件的情况,可以使用单例模式来缓存配置信息。
2)日志管理:日志记录器通常只需要一个实例来记录整个应用的日志信息。
3)线程池管理:线程池的创建和管理通常采用单例模式,因为线程池的创建代价较高。
4)对话框和任务栏:在GUI应用程序中,对话框或任务栏等组件通常只需要一个实例。
5)驱动程序:硬件驱动程序通常也需要单例模式,以确保只有一个驱动程序实例与硬件通信。
Spring 框架广泛使用了单例模式,特别是在依赖注入(DI)和容器管理中。
以下是一些 Spring 中使用单例模式的具体场景:
1)Bean 的单例模式:默认情况下,Spring 容器中的 Bean 是单例的。这意味着每个 Bean 只会被创建一次,并且在整个应用程序的生命周期内都可被复用。
单例模式的实现主要发生在 AbstractApplicationContext
和 AbstractBeanFactory
的层次上。
Spring 的核心是 BeanFactory
接口,它负责管理 Bean 的生命周期。
AbstractBeanFactory
是 BeanFactory
的抽象实现类,其中包含了单例模式的核心实现。
具体来看,AbstractBeanFactory
提供了一个 getSingleton
方法,用于获取单例 Bean 的实例。
如果实例不存在,则会创建一个,并将其存储在一个 Map
中。
java
protected <T> T getSingleton(String beanName, ObjectFactory<T> singletonFactory) throws BeansException {
synchronized (this.singletonObjects) {
// 如果已经存在,则直接返回
T obj = this.singletonObjects.get(beanName);
if (obj == null) {
// 如果不存在,则创建一个新的实例
if (this.logger.isDebugEnabled()) {
this.logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
}
obj = singletonFactory.getObject();
// 添加到单例缓存中
this.singletonObjects.put(beanName, obj);
// 添加到早期实例化列表
this.earlySingletonExposure.put(beanName, obj);
// 如果是智能初始化器,则进行初始化
if (obj instanceof SmartInitializingSingleton) {
this.registerSingletonInitCallback(beanName, (SmartInitializingSingleton) obj);
}
}
return obj;
}
}
这里的关键在于 getSingleton
方法,它首先检查 singletonObjects
Map 中是否已经存在该 Bean 的实例,如果不存在,则创建一个新的实例,并将其添加到 Map 中。
这样就确保了在整个应用中,该 Bean 只有一个实例。
2)ApplicationContext :ApplicationContext
接口的实现(如 FileSystemXmlApplicationContext
或 AnnotationConfigApplicationContext
)通常以单例形式存在,以便在整个应用程序中共享相同的上下文。
ApplicationContext
是 BeanFactory
的高级实现,它提供了更多的功能,如国际化支持、事件发布等。
ApplicationContext
默认使用 AbstractApplicationContext
实现,其中也包含了单例模式的应用。
java
@Override
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, null);
}
protected <T> T doGetBean(final String name, final Object requiredType, final Object dependencyCheck, final boolean typeCheckOnly)
throws BeansException {
final String beanName = transformBeanName(name);
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && recordSuppliedDependencies) {
if (!typeCheckOnly) {
markBeanUsage(beanName);
markBeanAsUsed(beanName);
}
return (T) getObjectForBeanInstance(sharedInstance, name, requiredType);
}
...
}
这里的 getBean
方法最终调用了 getSingleton
方法来获取单例 Bean 的实例。
3)AOP 代理:Spring AOP 产生的代理对象通常是单例的,除非特别指定了原型(Prototype)作用域。
4)事务管理器 :如 DataSourceTransactionManager
通常也是单例的,以便在整个应用中统一管理事务。
5)事件发布器 :如 ApplicationEventPublisher
接口的实现通常是单例的,以便统一管理事件的发布和监听。
Spring 框架通过
BeanFactory
和ApplicationContext
等核心组件广泛使用了单例模式,确保了 Bean 的单例特性。单例模式的实现主要集中在AbstractBeanFactory
的getSingleton
方法中,该方法通过一个Map
来缓存单例 Bean 的实例,确保了在整个应用中只有一个实例存在。这种实现方式不仅减少了资源消耗,还简化了对象的管理和访问。
参考文章:设计模式之单例模式(七种方法超详细)
一 叶 知 秋,奥 妙 玄 心