每日学习 设计模式 五种不同的单例模式

狮子大佬原文

https://blog.csdn.net/weixin_40461281/article/details/135050977

第一种 饿汉式

为什么叫饿汉,指的是"饿" 也就是说对象实例在程序启动时就已经被创建好,不管你是否需要,它都会在类加载时立即实例化,也就是说 实例化是在类加载时候完成的,早早的吃饱了

java 复制代码
   //饿汉单例
    public class Demo1{
        private static final Demo1 instance = new Demo1();
        public Demo1(){}
        public static Demo1 getInstance(){
            return instance;
        }
    }

优点:执行效率高,性能高,没有任何的锁

缺点:某些情况下,可能会造成内存浪费

第二种 懒汉式

懒汉指的是"懒"也就是说,实例对象的创建是延迟的,只有在第一次调用 getInstance() 方法时,才会创建单例对象。它不像饿汉模式那样在程序启动时就立即创建实例,而是在需要的时候才进行实例化。

优点:节省了内存,线程安全

缺点:性能低

三种创建方式

  • 第一种 不加锁
java 复制代码
    //懒汉单例
    public lass Demo2{
        private static Demo2 instance;
        public Demo2(){}
        public static Demo2 getInstance(){
            if (instance == null){
                instance = new Demo2();
            }
            return instance;
        }
    }

无法保证单例

  • 第二种 增加 synchronized 锁
java 复制代码
    //懒汉加锁
    public class Demo3{
        private static Demo3 instance;
        public Demo3(){}
        public synchronized static Demo3 getInstance(){
            if (instance == null){
                instance = new Demo3();
            }
            return instance;
        }
    }

可以保证单例 但性能较低 所有的线程全都被阻塞到方法外部排队处理

  • 第三种 双重校验单例
java 复制代码
   //懒汉双重校验
    public class Demo4{
        private static Demo4 instance;
        public Demo4(){}
        public  static Demo4 getInstance(){
            if (instance == null){
                synchronized(Demo4.class){
                    if (instance == null){
                        instance = new Demo4();
                    }
                }
            }
            return instance;
        }
    }

只锁创建方法提高性能,可以保证单例 性能还高 可以避免不必要的加锁

优点: 性能高了,线程安全了

缺点:可读性难度加大,不够优雅

第三种 枚举单例

在这种实现方式中,既可以避免多线程同步问题,还可以防止通过反射和反序列化来重新创建新的对象。

Java虚拟机会保证枚举对象的唯一性,因此每一个枚举类型和定义的枚举变量在JVM中都是唯一的。

java 复制代码
public enum Demo5 {

    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

INSTANCE 是 Demo5 枚举类的唯一实例。当程序运行时,Demo5.INSTANCE 就是该枚举类的唯一存在,也就是单例实例。

第四种 Spring中的单例模式实现 也可以称为 容器化单例

Spring 源码中的 DefaultSingletonBeanRegistry 类 getSingleton 方法

java 复制代码
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Quick check for existing instance without full singleton lock.
    Object singletonObject = this.singletonObjects.get(beanName); 
    // 尝试从 singletonObjects 缓存中直接获取已存在的单例对象。这个步骤不加锁,是为了提高性能。

    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 如果未找到单例对象,并且该单例对象正在创建中,进入下一个判断。
        singletonObject = this.earlySingletonObjects.get(beanName); 
        // 尝试从 earlySingletonObjects 缓存中获取提前引用的对象。

        if (singletonObject == null && allowEarlyReference) {
            // 如果 still 没有找到对象,并且允许提前引用时,尝试获取对象。
            if (!this.singletonLock.tryLock()) {
                // 如果无法获取锁,则避免在创建过程中返回提前引用,防止线程不安全的情况。
                return null;
            }
            try {
                // 在获取锁后,确保完整的单例创建过程。
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    // 如果单例对象还是没找到,进一步检查 earlySingletonObjects。
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        // 如果 earlySingletonObjects 中也没有找到,则需要从 singletonFactories 获取对象。
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            // 从 singletonFactories 中获取单例对象工厂,并调用 getObject() 创建对象。
                            singletonObject = singletonFactory.getObject();
                            // 获取对象后,检查该对象是否已添加或移除。
                            if (this.singletonFactories.remove(beanName) != null) {
                                // 如果工厂从 singletonFactories 中移除,说明创建了对象,放入 earlySingletonObjects 中。
                                this.earlySingletonObjects.put(beanName, singletonObject);
                            }
                            else {
                                // 如果对象被移除,说明对象已存在于 singletonObjects 中。
                                singletonObject = this.singletonObjects.get(beanName);
                            }
                        }
                    }
                }
            }
            finally {
                this.singletonLock.unlock(); 
                // 无论如何释放锁,保证线程安全。
            }
        }
    }
    return singletonObject; 
    // 返回找到的单例对象,如果找不到,则返回 null。
}

这里涉及到三个单例容器:

  • singletonObjects:
    这是最终存放已创建单例对象的缓存。正常情况下,当一个单例对象创建完成后,它会被放入这个缓存中,供后续的使用和访问。
    只有当对象完全创建完成且没有依赖其他对象时,它才会进入这个缓存。
  • earlySingletonObjects:
    当一个单例对象正在被创建时,可能有其他的 bean 依赖于它。为了防止这种依赖造成死锁或递归调用,Spring 会在对象创建的过程中将当前已经部分初始化的对象放到这个缓存中,供其他 bean 在创建过程中访问。
    这就是所谓的 "提前曝光",指的是在对象完全初始化之前,Spring 就让它能被其他依赖的 bean 使用。
  • singletonFactories:
    这个缓存中存放的是 ObjectFactory 对象,也就是单例对象的工厂。它们并不直接存储单例实例,而是存储生成单例实例的工厂。只有在没有找到单例对象(在前两个缓存中都找不到时),Spring 才会通过这些工厂来创建对象。
    这个缓存确保了在单例对象工厂可以提供实例之前,不会因为某个对象的引用而导致创建死锁。
    单例的获取顺序是singletonObjects -> earlySingletonObjects -> singletonFactories 这样的三级缓存

singletonObjects 指单例对象的缓存,singletonFactories 指单例对象工厂的缓存,earlySingletonObjects 指提前曝光的单例对象的缓存。

以上三个构成了三级缓存,Spring 就用这三级缓存巧妙的解决了循环依赖问题。

这里引发一个思考: 为什么要使用三级缓存才能解决循环依赖呢?这里转载一篇博客

原文链接:https://blog.csdn.net/qq_33204709/article/details/130423123

如果只使用一级缓存,我们可以根据上面的例子看到,类A和类B都不存在,根本没有初始化完成的对象可以存放到一级缓存中,所以循环依赖没有修复(死循环)

如果想打破上面循环依赖的死循环,就需要一个另一个缓存来将已经实例化但是没有完成依赖注入的对象给缓存起来这就是二级缓存。

然后再配合一级缓存,我们将创建好的单例对象存放到单例池中,同时清空二级缓存中对应的原始对象(半成品实例)

看到这里,我们就会有疑问,这不是一级缓存 + 二级缓存已经解决了循环依赖的问题了吗?为什么还需要三级缓存?

假如类A被增强了,那么我们需要注入到Bean容器中的就是A的代理对象,那么经过上面一整套流程下来,存放到一级缓存中的并不会是代理对象A,而是对象A。

为了将对应的代理对象A的实例也注入到容器中,这里我们就需要使用三级缓存了。

首先,我们在实例化A之后,将A中用于创建代理对象A的工厂对象 A-ObjectFactory,和B中用于创建对象B的工厂对象 B-ObjectFactor 放到三级缓存中。

并使用A的工厂对象 A-ObjectFactory 作为A的实例注入到A中。

然后,我们通过A的ObjectFactory对象创建A的代理对象(半成品/原始对象),然后将A的代理对象注入给B,就可以将B创建成功。

最后,我们将创建好的B放入单例池中,然后将B注入给A,这样我们就可以最终将A创建成功,然后将创建好的A再放入单例池中。

这样我们就成功使用三级缓存来解决了创建对象时的循环依赖的问题。

三级缓存只是解决了构造函数之后的循环依赖问题,那么构造函数的循环依赖问题怎么解决呢?

Spring 给我们提供了一个 @Lazy 注解,也叫懒加载,或延迟加载。被这个注解修饰的对象,只有在使用的时候才会创建实例,那时单例池中的其他对象都已经创建好了,便解决了循环依赖的问题。

第五种 特殊单例 线程单例

顾名思义 保证在所有线程内的单例

常见使用场景 日志框架 确保每个线程内都有一个单例日志实例 保证日志记录和输出的唯一性

在线程内最常使用的 TheadLocal 可以保证线程之间的变量隔离 基于他来实现线程单例

java 复制代码
public class ThreadLocalSingleton {
    // 通过 ThreadLocal 的初始化方法 withInitial 初始化对象实例 保证线程唯一
    private static final ThreadLocal<ThreadLocalSingleton> threadLocaLInstance =
            ThreadLocal.withInitial(() -> new ThreadLocalSingleton());

    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocaLInstance.get();
    }
}
相关推荐
世事如云有卷舒2 小时前
《ARM64体系结构编程与实践》学习笔记(三)
笔记·学习·arm
啄缘之间4 小时前
verilog练习:i2c slave 模块设计
学习·fpga开发·verilog·uvm
做怪小疯子4 小时前
跟着李沐老师学习深度学习(五)
学习
做怪小疯子6 小时前
跟着李沐老师学习深度学习(二)
人工智能·深度学习·学习
siy23336 小时前
[c语言日寄]跳水比赛名次问题:从逻辑推理到编程实现
c语言·开发语言·笔记·学习·算法
一个处女座的程序猿O(∩_∩)O7 小时前
React 设计模式:实用指南
前端·react.js·设计模式
扫地僧0097 小时前
第17章 读写锁分离设计模式(Java高并发编程详解:多线程与系统设计)
java·前端·设计模式
C182981825757 小时前
常用设计模式
设计模式
扫地僧0097 小时前
第19章 Future设计模式(Java高并发编程详解:多线程与系统设计)
java·python·设计模式
私人珍藏库7 小时前
windows10 wsa 安卓子系统终结版
学习