设计模式---单例模式
一、概述
单例模式涉及到一个类,该类负责创建自身的对象,并且每次创建出来的对象都是同一个,同时对外提供获取该类唯一对象的方法
二、分类
单例模式分为两种
1、懒汉式 : 类加载的时候便会创建该类的对象
2、饿汉式 : 类加载的时候不会创建对象,只有在使用的时候才会去创建该类的对象
三、案例
3.1、饿汉式
通过静态成员变量或者静态代码块的方式实现
1、静态成员变量方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class Singleton { // 构造方法私有化,防止外界创建对象,因为每次通过 new 的方式创建的对象都会在堆内存中开辟一块空间,创建出来的对象就不是同一个了,违背了单例模式的定义 private Singleton() { System.out.println( "通过构造方法创建对象" ); } private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } } // 测试类 public class SingletonDemo { public static void main(String[] args) { for ( int i = 1 ; i <= 5 ; i++) { Singleton instance = Singleton.getInstance(); System.out.println(instance); } } } |
2、静态代码块方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Singleton { private Singleton() { System.out.println( "通过构造方法创建对象" ); } private static Singleton instance; static { instance = new Singleton(); } public static Singleton getInstance() { return instance; } } |
静态成员变量和静态代码块方式实现单例模式本质上是一样的,通过上面的测试结果可以看出,构造方法被调用了一次,并且每次获取到的对象都是同一个,符合单例模式的要求
但是使用懒汉式单例模式有一个问题,比如有些对象我们暂时不需要使用,但是这些对象随着类的加载已经创建出来了,这势必会造成 内存浪费
3.2、懒汉式
饿汉式会造成内存空间的浪费,为了解决这个问题,我们还可以使用懒汉式的方式去实现单例模式,单例对象不随类的加载而创建,只有真正需要使用的时候才会去创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Singleton { private Singleton() { System.out.println( "通过构造方法创建对象" ); } private static Singleton instance; public static Singleton getInstance() { // 对象如果已经创建了则直接返回该单例对象 if (Objects.isNull(instance)){ // 代码 A instance = new Singleton(); // 代码 B } return instance; } } |
这种方式在单线程下是可行的,但是在并发条件下会有线程安全问题
线程 1 执行了代码 A,判断 instance 为空,在准备执行代码 B 的时候,CPU 的执行权被线程 2 抢占,此时 instance 仍为空
线程 2 获取到 CPU 执行权,执行代码 A,发现 instance 为空,然后执行代码 B,调用 new Singleton() 创建了一次对象,然后 CPU 的执行权又被线程 1 抢占了
线程 1 重新获取到 CPU 的执行权,执行代码 B,又调用构造方法,创建了 Singleton 的对象
可以看出,构造方法被调用两次,违背了单例模式的定义(每次创建出来的对象都是同一个)
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 测试代码 public class SingletonDemo { public static void main(String[] args) { Runnable runnable = () -> { Singleton instance = Singleton.getInstance(); System.out.println( "创建的对象内存地址值为: " + instance + "当前执行的线程是: " + Thread.currentThread().getName()); }; for ( int i = 0 ; i < 5 ; i++) { Thread thread = new Thread(runnable); thread.start(); } } } |
从结果可以看出,5 个线程调用了 5 次构造方法,创建出了 5 个不同 Singleton 对象,违背了单例模式原则,那么怎么解决并发条件下的线程安全问题呢,
可以使用 Synchronized 关键字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Singleton { private Singleton() { System.out.println( "通过构造方法创建对象" ); } private static Singleton instance; public synchronized static Singleton getInstance() { // 对象如果已经创建了则直接返回该单例对象 if (Objects.isNull(instance)){ instance = new Singleton(); } return instance; } } |
从上面的测试结果能看出,构造方法被调用一次,创建的对象都是同一个,符合单例模式,但是通过在方法上使用 synchronized 的方式会存在性能问题,多个线程调用该方法创建 Singleton 对象的时候都在同步等待,多线程就失去了意义
我们可以尝试缩小锁的粒度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class Singleton { private Singleton() { System.out.println( "通过构造方法创建对象" ); } private static Singleton instance; public static Singleton getInstance() { synchronized (Singleton. class ) { // 代码 A if (Objects.isNull(instance)) { // 代码 B instance = new Singleton(); // 代码 C } } return instance; } } |
这样就不需要同步整个方法了,只有在创建对象的时候才去同步,效率提高了,但是这样仍然会存在问题
线程 1 执行到代码 B 的时候,CPU 执行权发生切换
线程 2 获取到了 CPU 执行权,执行代码 A 的时候只能同步等待,CPU 执行权发生切换
线程 3 获取到了 CPU 执行权,执行代码 A 的时候也只能同步等待
同步等待和锁竞争的问题仍然没有解决,那么还有没有更好的办法呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class Singleton { private Singleton() { System.out.println( "通过构造方法创建对象" ); } private static Singleton instance; public static Singleton getInstance() { // 假设有 10 个线程同时调用 getInstance() 方法创建单例对象,此处判断如果已经存在 instance 对象,那么直接返回 // 没有则继续创建,降低了多线程环境下的锁竞争问题 if (Objects.isNull(instance)) { // 并发条件下,经过 Objects.isNull(instance) 过滤后,只会有少量的线程能执行到这里,假设最终只剩下 线程 1、线程 2、线程 3,这样就降低了需要同步等待的线程数量 synchronized (Singleton. class ) { if (Objects.isNull(instance)) { instance = new Singleton(); } } } return instance; } } |
10 个线程,最终只有线程 1、2、3 需要同步等待,降低了锁竞争次数,是不是这样就完美的解决了所有问题呢
其实并不是,一个对象的创建包含 3 个部分
1、在堆中为 Singleton 对象开辟内存空间
2、调用构造方法初始化对象,为成员变量赋值
3、将内存地址值指向堆中分配的内存空间
正常情况下依次执行步骤 1、2、3 之后,对象才算是真正的创建完成
但是步骤 2 与步骤 3 没有数据依赖关系,执行顺序是不确定的,如果先执行了步骤 3,然后再执行步骤 2,此时获取到的 Singleton 对象不为空,但是属性为空,对于属性,基本类型会有默认值,引用类型是 null,在具体业务使用对象中属性的时候就有可能抛出空指针异常
为了解决这个问题,需要禁止指令重排,这个时候就需要使用 volatile 关键字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class Singleton { private Singleton() { System.out.println( "通过构造方法创建对象" ); } // volatile 禁止指令重排 private static volatile Singleton instance; public static Singleton getInstance() { if (Objects.isNull(instance)) { synchronized (Singleton. class ) { if (Objects.isNull(instance)) { instance = new Singleton(); } } } return instance; } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?