2023.10.19 关于设计模式 —— 单例模式

news2024/9/22 7:30:34

目录

引言

单例模式

饿汉模式

懒汉模式

懒汉模式线程安全问题 

分析原因


引言

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

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

对象和类对象


单例模式

  • 单个实例(对象)
  • 在某些场景中有特定的类,其只能被创建出一个实例,不应该被创建多个实例
  • 而 单例模式 就针对上述的需求场景进行更强制的保证
  • 通过巧用 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/1122457.html

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

相关文章

VS采用nuget配置OpenGL

参考&#xff1a;如何配置Opengl编程环境_opengl配置_知心宝贝的博客-CSDN博客 这应该是最快的办法了&#xff0c;直接用nuget配置。 1.打开NuGet包管理器 2.搜索glew、glfw、glm、freeglut并点击安装即可 3.测试代码 能正常运行说明配置成功了 #include <GL/glew.h>…

Windows 安装 Java

1. 安装 JDK 从 Oracle 的官网下载的 JDK&#xff0c;例如 JDK 21 双击下载得到的 msi 文件&#xff0c;开始安装 JDK 选择要安装的文件路径&#xff08;我一般都默认&#xff09;&#xff1a; 等待安装&#xff1a; 安装完成&#xff1a; 2. 验证是否安装成功 2.1. 打开 cmd…

小插曲 -- 使用Visual Studio Code远程连接香橙派

在之前的学习中&#xff0c;代码的修改和保存都依赖于“vi”指令&#xff0c;而不得不承认vi指令的编辑界面非常原始&#xff0c;所以&#xff0c;如果可以将代码编辑放到更友好的环境里进行无疑是一件大快人心的事情。 本节介绍如何通过Visual Studio Code来进行远程连接: Vi…

Kubernetes 通过 Deployment 部署Jupyterlab

概要 在Kubernetes上部署jupyterlab服务&#xff0c;链接Kubernetes集群内的MySQL&#xff0c;实现简单的数据开发功能。 前置条件 镜像准备&#xff1a;自定义Docker镜像--Jupyterlab-CSDN博客 MySQL-Statefulset准备&#xff1a;StatefulSet 简单实践 Kubernetes-CSDN博客…

Nvidia显卡基础概念介绍

一、PCIe与SXM 1.1 Nvidia GPU PCIe PCIe(peripheral component interconnect express)是一种高速串行计算机扩展总线标准&#xff0c;是英特尔公司在2001年提出来的&#xff0c;它的出现主要是为了取代AGP接口&#xff0c;优点就是兼容性比较好&#xff0c;数据传输速率高、…

rust学习——字符串、字符串字面量、切片(slice)、字符串 slice

文章目录 字符串、字符串字面量、切片&#xff08;slice&#xff09;、字符串 slice01、字符串02、字符串字面量03、切片 &#xff08;slice&#xff09;04、字符串 slice 字符串 slice注意要点String 与 &str 的转换字符串深度剖析字符串 slice 作为函数参数例子001例子00…

SL8541 android系统环境+编译

1.Ubuntu系统的安装 最好使用ubuntu18.0.4 2.工具环境包的安装 // 安装Android8.1源码编译环境 sudo apt-get install openjdk-8-jdk --------------ok sudo apt-get install libx11-dev:i386 libreadline6-dev:i386 libgl1-mesa-dev g-multilib --------------ok sudo…

Git 保姆级使用教程

目录 一、Git介绍 二、Git 与 SVN 区别 三、Git 安装配置 1.Linux 平台上安装 Debian/Ubuntu Centos/RedHat 源码安装 2.Windows 平台上安装 3.Mac 平台上安装 四、安装完成后配置 五、创建版本仓库 六、Git常用命令 1.创建仓库命令 2.提交与修改 3.提交日志 4.…

高可用双机GPFS集群的的自动化部署脚本

1.环境说明&#xff1a; 系统主机名IP地址内存添加共享磁盘大小Centos7.9gpfs1192.168.10.1012G20GCentos7.9gpfs2192.168.10.1022G20G 2.配置共享硬盘&#xff1a; 前提&#xff1a;两台虚拟机没有拍摄快照 在mds001主机中&#xff1a; 添加五块5G的硬盘 SCSI > 创建新虚…

【Javascript】数组的基本操作

目录 声明 字面量形式 构造函数声明 访问数组中的元素 数组的长度 增删改查 增 通过索引添加数据 在数组后面添加数据 在数组前添加数据 删 删除数组中最后一个元素 删除数组中第一个元素 改 查 数组是⼀种列表对象&#xff0c;它的原型中提供了遍历和修改元素的…

力扣每日一题59:螺旋矩阵||

题目描述&#xff1a; 给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;[[1,2,3],[8,9,4],[7,6,5]]示例 2&#xff1a; 输入&am…

专业安卓实时投屏软件:极限投屏(QtScrcpy作者开发)使用说明

基本介绍 极限投屏是一款批量投屏管理安卓设备的软件&#xff0c;是QtScrcpy作者基于QtScrcpyCore开发&#xff0c;主要功能有&#xff1a; 设备投屏&控制&#xff1a;单个控制、批量控制分组管理wifi投屏adb shell快捷指令文件传输、apk安装 更多功能还在持续更新。 极…

【C语言小游戏--猜数字】

文章目录 前言1.游戏描述2.代码实现2.1打印菜单2.2构建基础框架2.3玩游戏2.3.1生成随机数2.3.1.1rand()2.3.1.2srand()2.3.1.3time() 2.3.2game() 2.4自己设定猜的次数 3.完整代码 前言 猜数字小游戏是我们大多数人学习C语言时都会了解到的一个有趣的C语言小游戏&#xff0c;下…

Milk-V Duo移植rt-thread smart

前言 &#xff08;1&#xff09;PLCT实验室实习生长期招聘&#xff1a;招聘信息链接 &#xff08;2&#xff09;首先&#xff0c;我们拿到Milk-V Duo板子之后&#xff0c;我个人建议先移植大核Linux。因为那个资料相对多一点&#xff0c;也简单很多&#xff0c;现象也容易观察到…

RK3568平台 GPIO子系统框架

一.gpio 子系统简介 gpio 子系统顾名思义&#xff0c;就是用于初始化 GPIO 并且提供相应的 API 函数&#xff0c;比如设置 GPIO为输入输出&#xff0c;读取 GPIO 的值等。gpio 子系统的主要目的就是方便驱动开发者使用 gpio&#xff0c;驱动 开发者在设备树中添加 gpio 相关信…

什么是网络编程?Java如何实现?三次握手和四次挥手?

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ Java网络编程 什么是网络编程&#xff1f;Java…

PAM从入门到精通(二十)

接前一篇文章&#xff1a;PAM从入门到精通&#xff08;十九&#xff09; 本文参考&#xff1a; 《The Linux-PAM Application Developers Guide》 先再来重温一下PAM系统架构&#xff1a; 更加形象的形式&#xff1a; 七、PAM-API各函数源码详解 前边的文章讲解了各PAM-API函…

SAP MM学习笔记38 - 入库/请求自动决济(ERS - Evaluated Receipt Settlement)

之前的章节学习了请求书的方方面面&#xff0c;这一章来个终章&#xff0c;入库/请求自动决济&#xff1a;&#xff09;。 1&#xff0c;什么是 ERS ERS&#xff0c;即 入库/请求自动决济&#xff0c;是 自動決済&#xff08;Automatic Settlement&#xff09;功能的一种。 以…

深入理解C++红黑树的底层实现及应用

文章目录 1、红黑树简介1.1 、概述&#xff1a;介绍红黑树的定义、特点和用途。 2、红黑树节点的定义3、红黑树结构3.1、红黑树的插入操作 4、红黑树的验证4.1、红黑树的删除4.2、红黑树与AVL树的比较4.3、红黑树的应用 5、总结 1、红黑树简介 1.1 、概述&#xff1a;介绍红黑…

视频剪辑教程:批量修改视频尺寸的简单方法

如果您需要批量修改大量视频的尺寸&#xff0c;这是一项繁琐且耗时的任务。但是&#xff0c;使用固乔剪辑助手&#xff0c;您可以通过简单的几个步骤轻松实现这一需求。下面是如何使用固乔剪辑助手来批量修改视频尺寸的步骤&#xff1a; 步骤1&#xff1a;导入视频素材 首先&am…