首页 > 其他分享 >单例模式的理论与实现

单例模式的理论与实现

时间:2023-08-13 10:56:04浏览次数:33  
标签:理论 private instance 实例 模式 单例 new public

本文实践代码仓库:https://github.com/goSilver/my_practice

目录

一、定义

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点以访问该实例。单例模式常用于需要共享资源或控制某个唯一资源的场景,例如数据库连接、线程池等。

二、作用

单例模式可以确保在整个应用程序中只有一个对象实例存在,从而方便地共享资源、管理状态或控制某些操作。

从业务概念上,有些数据在系统中只应该保存一份,就比较适合设计为单例类。比如,系统的配置信息类。除此之外,我们还可以使用单例解决资源访问冲突的问题。

三、实现

要实现一个单例,我们需要关注的点无外乎下面几个:

  • 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
  • 考虑对象创建时的线程安全问题;
  • 考虑是否支持延迟加载
  • 考虑 getInstance() 性能是否高(是否加锁)。

3.1 饿汉式

饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(在真正用到IdGenerator 的时候,再创建实例)。

如果实例资源初始化时间长、占用资源多,那么最好是采用饿汉式,将耗时的初始化操作提前到程序启动时就完成,避免程序在运行中发生崩溃。

public class Hungry {

    private final AtomicLong id = new AtomicLong(0);

    /**
     * 类加载时就初始化
     */
    private static final Hungry instance = new Hungry();

    private Hungry() {
    }

    public static Hungry getInstance() {
        return instance;
    }

    public long getId() {
        return id.incrementAndGet();
    }
}

3.2 懒汉式

懒汉式相对于饿汉式的优势是支持延迟加载。这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈。

public class Lazy {

    private final AtomicLong id = new AtomicLong(0);
    private static Lazy instance;

    private Lazy() {
    }

    /**
     * 获取实例的方法被synchronized关键字修饰
     * @return 实例
     */
    public static synchronized Lazy getInstance() {
        if (instance == null) {
            instance = new Lazy();
        }
        return instance;
    }

    public long getId() {
        return id.incrementAndGet();
    }
}

3.3 双重检查

双重检测实现方式既支持延迟加载、又支持高并发的单例实现方式。只要 instance 被创建之后,再调用 getInstance() 函数都不会进入到加锁逻辑中。所以,这种实现方式解决了懒汉式并发度低的问题。

public class DoubleCheck {
    private final AtomicLong id = new AtomicLong(0);
    /**
     * volatile关键字修饰
     * CPU 指令重排序可能导致在 IdGenerator 类的对象被关键字 new 创建并赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。
     * 这样,另一个线程就使用了一个没有完整初始化的 IdGenerator 类的对象。
     * 要解决这个问题,我们只需要给 instance 成员变量添加 volatile 关键字来禁止指令重排序即可。
     */
    private static volatile DoubleCheck instance;

    private DoubleCheck() {
    }

    /**
     * 双重检查
     * @return 实例
     */
    public static DoubleCheck getInstance() {
        if (instance == null) {
            // 只有第一次才会执行到这里,此处为类级别锁
            synchronized (DoubleCheck.class) {
                if (instance == null) {
                    instance = new DoubleCheck();
                }
            }
        }
        return instance;
    }

    public long getId() {
        return id.incrementAndGet();
    }
}

3.4 静态内部类

利用 Java 的静态内部类来实现单例。这种实现方式,既支持延迟加载,也支持高并发,实现起来也比双重检测简单。

public class StaticInnerClass {
    private AtomicLong id = new AtomicLong(0);
    private StaticInnerClass() {
    }

    /**
     * 静态内部类
     * SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。
     * 只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。
     * instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。
     */
    private static class SingletonHolder {
        private static final StaticInnerClass instance = new StaticInnerClass();
    }

    public static StaticInnerClass getInstance() {
        return SingletonHolder.instance;
    }

    public long getId() {
        return id.incrementAndGet();
    }
}

3.5 枚举

最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

public enum Enumm {
    /**
     * 实例
     */
    INSTANCE;
    private final AtomicLong id = new AtomicLong(0);
    public long getId() {
        return id.incrementAndGet();
    }
}

四、总结

4.1 单例存在哪些问题?

  1. 单例对 OOP 特性的支持不友好

一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。

  1. 单例会隐藏类之间的依赖关系

通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。

  1. 单例对代码的扩展性不友好

单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。

  1. 单例对代码的可测试性不友好

单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。

  1. 单例不支持有参数的构造函数

单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。

4.2 单例有什么替代解决方案?

为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。不过,静态方法这种实现思路,并不能解决我们之前提到的问题。如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,由程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。

有人把单例当作反模式,主张杜绝在项目中使用。我个人觉得这有点极端。模式没有对错,关键看你怎么用。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局的类,我们在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。

标签:理论,private,instance,实例,模式,单例,new,public
From: https://www.cnblogs.com/csh24/p/17626262.html

相关文章

  • 模式匹配
    第14章模式匹配模式匹配是检查某个值(value)是否匹配某一个模式的机制,一个成功的匹配同时会将匹配值解构为其组成部分。它是Java中的switch语句的升级版,同样可以用于替代一系列的if/else语句。模式匹配有点像一个别致的switch声明,它可以侵入到表达式数据结构内部,对这个结构进行......
  • 多变量信息理论
    多变量信息理论是一种用于研究多个变量之间相互作用的方法。它可以帮助理解多个变量之间的关系,包括相关性、依赖性和交互作用等方面。以下是多变量信息理论的一些基本概念和应用:1.熵:熵是信息理论中的一个基本概念,用于衡量随机变量的不确定性。在多变量信息理论中,可以使用条件熵......
  • 完全背包理论基础
    完全背包跟01背包的代码区别就是在第二层背包的遍历的时候是正序的!先遍历物品还是背包是一样的//先遍历物品,再遍历背包privatestaticvoidtestCompletePack(){int[]weight={1,3,4};int[]value={15,20,30};intbagWeight=4;int[]dp=newint[b......
  • 【Java】智慧工地源码-支持私有化部署,SaaS模式+全套硬件设备
    智慧工地硬件设备包括:AI识别一体机、智能广播音响、标养箱、塔机黑匣子、升降机黑匣子、吊钩追踪控制设备、扬尘监测设备、喷淋设备。1.什么是AI危险源识别AI危险源识别是指基于智能视频分析技术,对视频图像信息进行自动分析识别,以实时监测危险区域的人员闯入、靠近等危险行为,从......
  • 11 外观模式 -- go语言设计模式
    外观模式又称为门面模式,它是一种结构型模式。引入外观模式后调用方与多个子系统的通信必须通过一个统一的外观对象进行,外观模式为子系统中的功能接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这些子系统更加容易使用。外观模式的实现代码packagemainimport"......
  • 简单工厂模式
     目录前言导语代码部分总结前言我是歌谣我有个兄弟巅峰的时候排名c站总榜19叫前端小歌谣曾经我花了三年的时间创作了他现在我要用五年的时间超越他今天又是接近兄弟的一天人生难免坎坷大不了从头再来歌谣的意志是永恒的放弃很容易但是坚持一定很酷导语简单工厂模式编辑......
  • CAP 理论
    CAP理论基本概念维基百科的翻译版本在理论计算机科学中,CAP定理(CAPtheorem),又被称作布鲁尔定理(Brewer’stheorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:一致性(Consistency):等同于所有节点访问同一份最新的数据副本;可用性(Availability):每次请求......
  • Java 观察者模式的浅析
    简单地说,观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监察一个主题对象。这样一个主题对象在状态上的变化能够通知所有的依赖于此对象的那些观察者对象,使这些观察者对象能够自动更新。 观察者模式的结构 观察者(Observer)模式是对象的行为型模式,又叫做发表-订阅(P......
  • Java单例模式详解
    Java单例模式详解单例模式是设计模式中的一种,它确保某一个类只有一个实例,并提供一个全局点来访问这个实例。这在某些场景中是非常有用的,例如,配置管理、线程池、缓存、日志对象等。1.单例模式的基本原则:构造函数是私有的。有一个私有静态变量来保存类的唯一实例。有一个公有静态方......
  • 中介者模式-19
    概述中介者模式(MediatorPattern)又称调停者模式。它定义一个中介对象封装一系列对象的交互,使得交互的对象不需要显式地引用。优点:简化对象之间的交互,减少子类的数量。缺点:中介类可能非常复杂。abstractclassAbstractMediator{abstractvoidregister(Memberm);a......