设计模式 - 单例模式

本文深入解析单例模式的设计原理及应用场景,对比多种实现方式如饿汉式、懒汉式、双检锁等,并探讨其优劣及适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我使用单例模式的初衷就是保持同一个对象,内部为同一份数据,同时全局均可调用 ~

设计模式分为三种类型,共23种

  • 创建型模式:单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式
  • 结构型模式:代理模式、装饰模式、外观模式、享元模式、桥接模式、组合模式、适配器模式
  • 行为型模式:观察者模式、策略模式、中介者模式、模版方法模式、命令模式、迭代器模式、职责链模式(责任链模式)、备忘录模式、解释器模式(Interpreter模式)、状态模式、访问者模式

基础了解

单例模式属于创造型模式的一种,在开发中也是最为常用的一种设计模式,其存在实现了数据同步化,同时减少了内存的开支,提升了一定的开发效率~

单例特性

  • 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  • 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new 实例 ,主要实现了统一出口的原则,在开发中一定程度上减少了耦合性

适用场景

  • 数据需要在全局统一调用、更改等,需要保持数据的一致性
  • 对象只允许在独一份,保证唯一性
  • 需要频繁的进行创建和销毁的对象(经常用到的对象)
  • 创建对象时耗时过多或耗费资源过多(常见于一些工具类对象)
  • 频繁被访问的数据库或文件对象 (比如 数据源、session 工厂等)

实战演练

个人使用 双检锁模式 多一点,因为可以既可以保证线程安全性,又能解决掉一些线程并发的场景 ~

饿汉式

核心在于饿字,体现了饥不择食的状态,不希望等待,所以当使用该实例时 每次都会返回一个新建的实例,内存开销相对交大

优点:写法简单,就是在类装载的时候就完成实例化,避免了线程同步问题
缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果;不论是频繁调用或从未使用都会造成内存的浪费!

public class Singleton {
    public Singleton() {}

    public static Singleton singleton = new Singleton();

    //静态工厂方法
    public static Singleton getInstance() {
        return singleton;
    }
}

懒汉式

总感觉这部分描述有些不太正确,以后若有机会再来优化

核心在于字,意味着 除首次新建实例外,之后若再次调用此实例的话,均是同一实例,减少了内存开销;同时可以保证我们操作的目标为同一目标

在多线程并发的场景下,这种方式并不能保证线程安全

常规而言懒汉式的具体方式其实有三种,主要与synchronized是否声明、声明位置有关(介于双检索方式的一些不太正确的写法)

  • 线程不安全
  • 线程安全,同步方法
  • 线程安全,同步代码块

在多线程中要注意同步代码块,否则容易在if 的判断期间执行多次实例创建 ~ 有的人会说这样做效率比较低,其实我想说真低不到哪里去 ~ 我认为除非是代码优化到一定高度才在这里进行二次优化

public class Singleton {
    private Singleton() {}

    private static Singleton singleton;

    //静态获取实例,没有就创建,有就返回
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

双检锁

在线程并发中有三大特性:原子性、可见性、有序性

双检锁方式也称双锁检验,我们可以把它看做是基于懒汉式的优化校验,其中主要涉及到了synchronized volatile 关键字

  • synchronized 主要作用于有序性,保证同时间尽可能只有一条线程进行操作
  • volatile 主要作用于数据的可见性,当有线程更新数据源后及时刷新数据,防止并发造成数据错误

关于 getInstance() 判断条件,首次判断主要查看实例(对象)是否存在,二次判断主要是确保对象的一致性,保证线程安全

public class Singleton {
    private Singleton() {}

    private static volatile Singleton singleton; //volatile修饰 保证修改的值会立即被更新到主存

    public static Singleton getInstance() {
    	//非空校验,第一把锁
        if (singleton == null) {
        	//线程安全,二次判断(相当于第二把锁)
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

静态内部类

静态内部类方式区别于以上方式,相比实现更简单,不过这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用

  • 类的静态属性只会在第一次加载类(调用 getInstance())的时候才会初始化
  • JVM 特性帮助我们保证了线程的安全性,在类进行初始化(单例对象创建)时,别的线程是无法进入的
public class Singleton {
    private Singleton() {}

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

枚举

简洁清晰,自动支持序列化机制,绝对防止多次实例化(没咋用过,仅用于学习记录

public enum Singleton {
    INSTANCE;

    public void method() {

    }
}

思维扩展

以下内容为自我答疑过程,以后或许会移植到一篇新Bolg内

  • 一般情况下,饿汉式、懒汉式(包含线程安全和线程不安全俩种方式)都用的比较少;
  • 常规在项目中双检锁方式用的更多一些
  • 在要明确实现 lazy loading 效果时,可以考虑静态内部类的实现方式;
  • 若涉及到反序列化创建对象时,大家也可以尝试使用枚举方式

内部类场景

  • 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据
  • 内部类可以对同一个包中的其他类隐藏起来
  • 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷

静态域、公有域和实例域的区别

什么是域?在我认为成员变量和局部变量就是域的一种体现,他们因域的不同所以执行效果也不尽相同 ~

静态域 = 公有域
将域定义为static,一个类中只有一个这样的域,

对象首次初始化时将copy一份到堆内,方法区外,作为静态对象 ;而带static修饰的类,方法,字段,代码块都会被copy一份到静态域中,同时独立且唯一地存在于这个静态对象中 ~ 有的人喜欢称其为共享变量 ~

针对于被static修饰的变量、代码块、方法、内部类

变量: 静态变量会copy一份到堆内, 方法区外的静态对象中,那么它属于静态域,可被所有线程共享,一旦成为共享变量后,最好使用原子操作类替代

代码块:静态代码块会copy一份到堆内,方法区外的静态对象中,那么它属于静态域,只会在第一次初始化对象时执行一次,不可访问非静态的部分

方法、内部类: 静态方法或静态内部类会copy一份到堆内, 方法区外的静态对象中,那么它属于静态域,不可访问非静态的部分,但可被所有线程共享

实例域
每一个对象对于所有的实例域都有自己的一份拷贝,所有的非final,非static的对象都存储在实例域中,只可被当前实例查看及更新

域的初始化与赋值

两种情况

  • 在建立对象即进行类的实例化时域的初始化
  • 在不建立对象,只装载类的时候域的初始化

有两种情况是只装载类而不实例化类

  • java classname 执行程序时
  • classname.statement 调用类的静态域或静态方法时

赋值方式

  • 赋予默认赋值
  • 声明变量时同时赋值
  • 块赋值(实例块和静态块)
  • 构造器赋值

静态内部类和非静态内部类之间得不同

  • 内部静态类不需要有指向外部类的引用;但非静态内部类需要持有对外部类的引用
  • 非静态内部类能够访问外部类的静态和非静态成员;静态类不能访问外部类的非静态成员。他只能访问外部类的静态成员
  • 一个静态内部类不能脱离外部类实体被创建;一个非静态内部类可以访问外部类的数据和方法,因为他就在外部类里面

懒汉式和饿汉式的区别

相对代码而言有俩点

  • 懒汉式多一个if的判空
  • 饿汉式不需要if的判空

相比思想而言

  • 特点也在其之上的基础,懒汉式首先的初始化,同时之后每次都会通过判空才进行处理,如果其已经存在的话,直接调用已有的那个值
  • 饿汉式而言呢,在于每一次去查找实例的时候,直接就是返回一个新的对象。

结果
俩者相比而言,属于本质区别的!懒汉式是创建一次实例一直使用,饿汉式是每次都要开启新的对象,浪费内存~

饿汉式与静态内部类的区别

俩种方式机制类似,但又有不同 ~

  • 相同点
    都采用了类装载的机制来保证初始化实例时只有一个线程
  • 不同点
    饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用 ~
    静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化 ~

方式分类

此处是看别人文章而来,记录一番

  • 枚举 好于 饿汉:占用资源少,不需要延时加载
  • 静态内部类 好于 懒汉式:占用资源多,需要延时加载
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

远方那座山

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值