2023.10.19 关于 单例模式 详解

news2025/1/14 18:04:28

目录

引言

单例模式

饿汉模式

懒汉模式

懒汉模式线程安全问题 

分析原因


引言

  • 设计模式为编写代码的 约定 和 规范

阅读下面文章前建议点击下方链接明白 对象 和 类对象

对象和类对象


单例模式

  • 单个实例(对象)
  • 在某些场景中有特定的类,其只能被创建出一个实例,不应该被创建多个实例
  • 而 单例模式 就针对上述的需求场景进行更强制的保证
  • 通过巧用 java 的现有语法,实现了某个类只能被创建出一个实例的效果
  • 从而当程序员不小心创建出多个实例时,会编译报错

实例理解:

  • JDBC 编程中的 DataSource 类,我们仅连接一个数据库时
  • DataSource 类描述数据的来源,用于获取数据库连接,因为数据只来源于一个数据库,所以我们仅创建一个实例即可,无需创建多个实例
  • 从而该场景适合使用单例模式

具体思路:

  •  static 关键字可以将成员变量或方法声明为静态的,让它们属于类级别而不是实例级别
  • 因此我们可以将 单例对象 声明为静态变量,让其可以在任何地方直接通过类名来访问,无需创建类的实例
  • 从而这样便可以方便地获取单例对象,并且对于多个调用者来说,始终返回同一个实例

简单理解:

  •  static 关键字将 单例对象 转为 静态变量
  • 因此 单例对象 便从 与实例相关联 转为 与类相关联
  • 又因为 在一个 java 进程中,对于同一个类,只会存在一个对应的类对象
  • 所以该 类对象内部的类属性也仅会存在一份,也就是作为类属性的 单例对象 也仅会存在一份

饿汉模式

// 饿汉模式的 单例模式 实现
// 此处保证 Singleton 这个类只能创建出一个实例
class Singleton {
//    在此处,先把这个实例给创建出来了
//    使用 private 修饰是为了防止在类外对 Singleton 实例 instance 进行修改
    private static Singleton instance = new Singleton();

//    如果需要使用这个唯一实例,统一通过 Singleton.getInstance() 方式来获取
    public static Singleton getInstance() {
        return instance;
    }

//    为了避免 Singleton 类不小心被复制出来多份
//    把构造方法设为 private ,在类外面就无法通过 new 的方式来创建这个 Singleton 实例了
    private Singleton() {}
}

public class ThreadDemo19 {
    public static void main(String[] args) {
        Singleton s = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s == s2);
    }
}
  • 该模式表示在类加载阶段,就已经把实例创建出来了
  • 所以 "饿汉" 一词便体现出创建该实例的急迫感

懒汉模式

class SingletonLazy {
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if(instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    private SingletonLazy(){}
}

public class ThreadDemo20 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}
  • 该模式在创建实例时并非是在类加载阶段,就已经把实例创建出来了
  • 而是当真正第一次使用的时候才创建实例
  • 所以相比于 "饿汉" 模式创建实例的急切感,"懒汉" 模式则显得没那么着急

阅读下面文章之前建议点击下方链接了解清楚线程安全问题

线程安全问题详解


懒汉模式线程安全问题 

  • 相比于 饿汉模式 仅涉及到读操作
  • 懒汉模式 则既涉及到 写操作 又涉及到 读操作

  • 显然 懒汉模式 有着线程安全问题

分析原因

  • 懒汉模式线程安全问题的本质为 读操作、比较操作、写操作 这三个操作并不是原子的
  • 从而便会导致线程t2 读到的 instance 值可能是线程t1 还没来得及写的
  • 这也就是我们常说的 脏读

  • 此时我们便可以利用 synchronized 关键字来进行加锁,使得上图中的指令变为原子的
public static SingletonLazy getInstance() {
        synchronized (SingletonLazy.class) {
            if(instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
  • 加锁的对象是 SingletonLazy.class 类对象
  • 该锁是基于类的

  • 虽然对 SingletonLazy.class 类对象进行加锁能解决多线程之间脏读的问题
  • 但是也导致了每次调用 getInstance 方法时都需要先进行加锁,才能进入方法内部进行判断 instance 是否为空,非空则触发 return 直接返回单例对象
  • 我们要清楚的一点是 加锁操的开销还挺大,会涉及到用户态到内核态之间的切换,这样切换成本的成本是很高的
  • 要注意到的是 在 new 完单例对象之后,后续再调用 getInstance 方法时,我们仅会直接返回单例对象,即仅涉及到读操作,这是没有线程安全问题的
  • 所以在 new 出对象之前有加锁操作,这是十分有必要的,即任意线程第一次调用getInstance 方法
  •  在 new 完单例对象之后,我们无需再进行加锁操作,这样便可以很大程度上提高效率
public static SingletonLazy getInstance() {
        if (instance == null){
            synchronized (SingletonLazy.class) {
                if(instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
  • 我们便可以在 加锁操作 的外层再加上个 if 判断,判断 instance 对象是否已经被创建出来了
  • 从而该代码只会在任意线程第一次调用 getInstance 方法时,才会进行加锁操作
  • 从而此处不再是无脑加锁,而是满足了特定条件之后,才真正加锁

  • 我们需要理解此处为什么会有两个相同的 if 判断
  • 首先如果这两个 if 判断之间没有加锁操作,那么写两个一模一样 if 判断是毫无意义的
  • 但是正因为这两个 if 判断之间有加锁操作,而加锁操作就可能会引起线程阻塞,当线程竞争到锁之后,再执行到第二个 if 判断的时候,可能与第一次执行 if 判断之前隔了很长一段时间

举例理解:

  • 线程A 第一次调用 getInstance 方法,读取到 instance 为 null,通过第一次 if 判定,并成功为锁对象进行加锁操作,然后再次读取到 instance 为 null,通过第二次 if 判定,进而直接 new 出一个 instance 对象,最后再将锁释放
  • 可能线程B 比线程A 晚一点点的调用了 getInstance 方法,可能此时线程A 并未修改完instance 的值,从而线程B 读取到 instance 为 null,通过了第一次 if 判定,然后进行阻塞等待线程A 释放锁,但正是在线程B 等待锁的在这段时间里,线程A 已经将 instance 对象给创建出来了,此时线程B 再获取到锁时,instance 的值已经发生改变了,线程B 再次读取 instance 的值,此时 instance 不为 null,从而未通过第二次 if 判断,直接返回 instance 的值,这就意味着第二次 if 判断成功阻止了线程B 再创建一个新的 instance 对象
  • 根据上述例子,深入理解 图中第一个 if 负责判定是否要加锁,解决了每次调用getInstance 方法时都需要引入无意义的加锁操作,很大程度上减少了开销,第二个 if 负责判定是否要创建对象,是最初为了保障单例模式,引入的必要条件
  • 这两 if 判断的目的是完全不相同的,只是碰巧代码是一样的!

  • 上述仅解决了多线程之间 脏读 的问题,但是还可能会有 内存可见性问题
  • 假设有很多线程,都去执行 getInstance 方法,这个时候便可能存在被优化的风险,即只有第一次读才是真正读了内存,后续都是读寄存器或 cache 
  • 同时还可能涉及到 指令重排序问题
  • 编译器为了提高程序的效率,调整代码执行顺序
  • 即 我们可以将 instance = new Singleton(),拆分为三个步骤
  • 步骤 1:申请内存空间
  • 步骤 2:调用构造方法,把这个内存空间初始化成一个合理的对象
  • 步骤 3:把内存空间的地址赋值给 instance 引用
  • 编译器可能将步骤的执行顺序由 1、2、3,优化重排序为 1、3、2
  • 如果仅是在单线程场景下,执行步骤的调换是没有任何影响的
  • 但是如果是在多线程环境下,我们举一个简单例子来理解指令重排序所带来的问题

举例理解:

  • 假设编译器优化指令重排序,线程A 的步骤执行顺序变为 1、3、2,如果线程A 执行完步骤 1、3,正当要执行步骤 2 时,被切出 CPU,CPU 调度执行线程B
  • 我们要注意到的是,此时线程A 执行完步骤 1、3 后会创建出一个非法对象,即该对象仅分配了内存,其数据是无效的,只有执行完步骤 2 才会把这个内存空间初始化成一个合理的对象
  • 那么当 CPU 调度执行线程B 时,线程B 又正好调用 getInstance 方法,此刻便会进入第一个 if 判断,获取 instance 对象的值,来判断是否为 null
  • 因为 instance 对象 已经被分配好了内存空间,所以线程B 获取到的 instance 对象值并不会为 null
  • 所以线程B 将会直接返回该 instance 对象
  • 注意此处线程B 返回的 instance 对象 是上述讲的非法对象,即仅分配了内存,其数据是无效的
  • 所以之后 线程B 拿着这个非法对象,来进行使用便将会出现许多问题和错误

解决方法:

  • 引入 volatile 关键字
  • volatile 关键字的功能正好能解决 内存可见性 和 指令重排序
class SingletonLazy {
    private volatile static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if (instance == null){
            synchronized (SingletonLazy.class) {
                if(instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy(){}
}

public class ThreadDemo20 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}
  • 以上完整的代码便是 线程安全的懒汉模式 完全体

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1111864.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python学习-----Day09

一、利用装饰器来获取函数运行的时间、 #导入time模块 import timedef decorated(fn):def inner():#time.time获取函数执行的时间a time.time() # func开始的时间fn()b time.time() # func结束的时间print(f"{fn.__name__}程序运行的总数时间:{b - a}秒")return…

View 自定义 - 绘制前的准备 DecorView

一、概念 图中可以看出 ViewRoot 最后一步是绘制,在绘制之前系统会有一些准备,即前面几个步骤:创建PhoneWindow、DecorView、ViewRootmpl。 二、DecorView 的创建 DecorView的创建开始是从 Activity 中 setContentView() 开始的。 创建抽象类…

项目管理的10个经典法则

大家好,我是老原。 前两天给大家更新了一篇实用文,分享了6张能搞定项目管理的思维导图,很多粉丝朋反馈对项目更有思路了。 那6张思维导图都需要建立在一定的项目管理法则上,今天我也给你们整了项目管理的10个经典法则。 建议大…

域控主机 带瘤

1.装环境 是dns环境 加入域 二.文件上传 2.cs木马生成 服务器 75.233 上传木马成功 上线

C语言之通讯录的实现篇优化版

目录 动态内存管理 通讯录声明 静态版本 动态版本 ​初始化通讯录 静态版本 动态版本 Add增加通讯录 静态版本 动态版本 Checkcapacity增容 DestroyContact释放动态空间 文件操作 SaveContact保存信息到文件中 初始化通讯录 旧版本 文件版本 LoadContact加载…

Hudi-源码-索引-bloom 索引

文章目录 前言问题原理TagLocation流程入口LookupIndexfindMatchingFilesForRecordKeysHoodieKeyLookupHandle 如何优化问题一 如何避免大量 IO问题二 如何减少计算 Hash问题三 使用什么结构优化比对结果如何初始化树查询 总结 前言 Hudi 系列文章在这个这里查看 https://gith…

图论与网络优化

2.概念与计算 2.1 图的定义 2.1.1 定义 图(graph) G G G 是一个有序的三元组&#xff0c;记作 G < V ( G ) , E ( G ) , ψ ( G ) > G<V(G),E(G),\psi (G)> G<V(G),E(G),ψ(G)>。 V ( G ) V(G) V(G) 是顶点集。 E ( G ) E(G) E(G) 是边集。 ψ ( G ) \…

【合集】Redis——Redis的入门到进阶 结合实际场景的Redis的应用

前言 Redis是一个开源的内存数据结构存储系统&#xff0c;也被称为键值存储系统。它支持多种数据结构&#xff0c;如字符串、哈希表、列表、集合、有序集合等&#xff0c;并提供了丰富的操作命令&#xff0c;可以对这些数据结构进行快速的读写操作。Redis具有高性能、高可用性…

驱动:驱动相关概念,内核模块编程,内核消息打印printk函数的使用

一、驱动相关概念 1.操作系统的功能 向下管理硬件&#xff0c;向上提供接口 操作系统向上提供的接口类型&#xff1a; 内存管理&#xff1a;内存申请&#xff08;malloc&#xff09; 内存释放&#xff08;free&#xff09;等 文件管理&#xff1a; 通过文件系统格式对文件ext2…

this指向详解

目录 一&#xff1a;严格模式与非严格模式 1.严格模式的开启 2.this指向的一些情况&#xff1a; 二&#xff1a;如何指定this的值&#xff1f; 1.在调用时指定this的值 2.在创建时指定this的值 ​编辑三&#xff1a; 结尾 一&#xff1a;严格模式与非严格模式 在非严格模…

项目管理之分析项目特点的方法

在管理项目时&#xff0c;了解项目的目标和实现方法可以帮助我们更好地规划和执行项目。根据项目的目标和实现方法的不同&#xff0c;可以将项目分为四种类型&#xff1a;地、水、火和气。 对于工程项目&#xff0c;采用基于活动任务的计划管理方法&#xff0c;使用活动网络图…

聊聊分布式架构08——SpringBoot开启微服务时代

目录 微服务架构时代 快速入门 入门详解 SpringBoot的自动配置 石器时代&#xff1a;XML配置bean 青铜时代&#xff1a;SpringConfig 铁器时代&#xff1a;AutoConfigurationImportSelector 手写简单Starter SpringApplication启动原理 微服务架构时代 Spring Boot的…

LabVIEW中将枚举与条件结构一起使用

LabVIEW中将枚举与条件结构一起使用 枚举是一个具有相应数值的字符串标签型列表。在LabVIEW&#xff08;U8 &#xff0c; U16-默认值和U32&#xff09;中以无符号整数形式应用。 例如&#xff0c;可以有一个枚举保存四个季节&#xff0c;在这种情况下&#xff0c;每个字符串都…

2022最新版-李宏毅机器学习深度学习课程-P26RNN-2

一、RNN网络结构 与时间有关的反向传播&#xff08;每次不同&#xff09; 损失函数 实验其实不容易跑&#xff0c;因为他的损失函数曲线幅度很大 画出来差不多是这个样子。突然一下升高是因为从右到左碰到陡峭的地方梯度一下变大了&#xff0c;所以弹回去了。 原作者在训练时…

JAVA反射(原理+使用)

引言 反射是一种机制&#xff0c;能够使java程序在运行过程中&#xff0c;检查&#xff0c;获取类的基本信息&#xff08;包&#xff0c;属性&#xff0c;方法等&#xff09;&#xff0c;并且可以操作对象的属性和方法 反射是框架实现的基础 反射的原理 讲述反射的原理之前&a…

covfefe 靶机/缓冲区溢出

covfefe 信息搜集 存活检测 详细扫描 后台网页扫描 80 端口 31337 端口 网页信息搜集 分别访问扫描出的网页 说有三个不允许看的内容 尝试访问 第一个 flag 访问 .ssh 文件 继续根据提示访问 获取了三个 ssh 文件 ssh 登录 在下载的 id_rsa_pub 公钥文件中发现了…

leetCode 11. 盛最多水的容器 + 双指针

11. 盛最多水的容器 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/container-with-most-water/description/?envTypestudy-plan-v2&envIdtop-interview-150 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是…

【Java基础面试三十三】、接口和抽象类有什么区别?

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;接口和抽象类有什么区别…

大同小异!如何在苹果不同类型设备上更改AirDrop的名称

你可以更改你的AirDrop ID&#xff0c;让其他人看到你名字之外的东西。本文介绍了如何在iPhone、iPad和Mac上更改AirDrop名称。 如何在iPhone上更改AirDrop名称 在iPhone上更改AirDrop名称涉及到你可能不想做的更改。幸运的是&#xff0c;这在iPad和Mac上不是真的&#xff0c…

【408数据结构】第一章 绪论

第一章 绪论 1.数据结构基本概念及三要素 一.数据结构基本概念 1.数据 信息的载体&#xff0c;能被客观事物描述的数字&#xff0c;字符以及能被计算机程序识别和处理的符号的集合 2.数据元素 数据的基本单位&#xff0c;一个数据元素可由若干个数据项&#xff08;构成数…