多线程——单例模式

news2024/10/21 18:33:37

目录

·前言

一、设计模式

二、饿汉模式

三、懒汉模式

1.单线程版

2.多线程版

·结尾


·前言

        前面的几篇文章中介绍了多线程编程的基础知识,在本篇文章开始,就会利用前面的多线程编程知识来编写一些代码案例,从而使大家可以更好的理解运用多线程来编写程序,本篇文章会用多线程来实现设计模式中的“单例模式”,这里实现“单例模式”的方式主要介绍两种:“饿汉模式”和“懒汉模式”,下面进行本篇文章的重点内容吧。

一、设计模式

        本篇文章介绍的单例模式属于设计模式中的一种,那么什么是设计模式呢?设计模式和象棋中的“棋谱“”比较类似,比如“红方当头炮,黑方马来跳”,针对红方的一些走法,黑方应招也有一些固定的套路,按照这种套路来下,局势就不会吃亏,按照棋谱下棋,下出来的棋不会太差,因为棋谱会兜住我们下棋的下限,设计模式也是如此,按照设计模式来写代码同样可以兜住我们的下限。

        单例模式,是设计模式的一种,它可以保证某个类在程序中只存在唯一的一份实例,而不会创建出多个实例,这点需求在很多场景都需要,比如在我们前面 MySql 篇章 JDBC 编程中的 DataSource 实例就只需要一个。使用单例模式,就可以对我们的代码进行一个更严格的校验和检查,不会像口头约定那样还可以创建多个实例。

        单例模式的具体实现有很多种,本篇文章就来介绍两种实现方式:“饿汉模式”和“懒汉模式”。

二、饿汉模式

        饿汉模式下实现的单例模式,在类加载时就会创建好对象实例,具体的代码已经运行示例如下所示,通过代码中的注释对代码再进一步介绍:

// 希望这个类在进程中只有一个实例
class Singleton{
    private static Singleton instance = new Singleton();
    // get 方法设为静态方法,这样其他代码想要使用这个类的实例就需要通过这个方法来获取
    // 不应该在其他代码中重新 new 这个对象,而是使用这个方法获取现成的对象
    public static Singleton getInstance() {
        return instance;
    }
    // 将构造方法设为 private 这样其他代码中就无法通过构造方法再进行实例化一个新对象
    private Singleton() {}
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        // 利用"饿汉模式"实现的单例模式创建两个对象,观察这两个对象是否相同
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1==s2);
    }
}

        上述的代码就是“饿汉模式”单例模式中一种简单的实现方式,这里实例是在类加载的时候就创建了,创建的时机非常早,这就相当于程序一启动,实例就创建好了,就使用“饿汉”来形容“创建实例非常迫切,非常早”。

三、懒汉模式

        懒汉模式下实现的单例模式,在类加载的时候不创建实例,在第一次使用的时候才创建实例。这样的设计方式可以节省一些不必要的开销,以生活中的肯德基疯狂星期四为例,只有在星期四时,肯德基的点餐小程序上才会出现疯狂星期四的特价餐品,此时使用懒汉模式,不是星期四时就不会加载疯狂星期四的特价餐品,就会节省一些开销。

1.单线程版

        下面来以懒汉模式来实现一个单线程版的单例模式,示例代码及运行结果如下所示:

// 懒汉模式---单线程版
class SingletonLazy{
    // 这个引用指向唯一实例,初始化为 null,而不是立即创建实例
    private static SingletonLazy instance = null;
    private SingletonLazy() {}
    public static SingletonLazy getInstance() {
        if (instance == null){
            // 首次调用 getInstance 方法,创建实例
            instance = new SingletonLazy();
        }
        // 如果不是第一次调用 getInstance 方法,直接返回之前创建好的实例
        return instance;
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        // 利用"懒汉模式"实现的单例模式创建两个对象,观察这两个对象是否相同
        SingletonLazy b = SingletonLazy.getInstance();
        SingletonLazy b2 = SingletonLazy.getInstance();
        System.out.println(b==b2);
    }
}

        由运行结果可以看出,上述的代码写法仍然可以保证该类的实例是唯一一个,与此同时,创建实例的时机就不是程序启动时了,而是第一次调用 getInstance 方法的时候。 

2.多线程版

        通过上面单线程版的懒汉模式实现单例模式,我们可以来分析一下上述的代码是否是线程安全的呢?结论一定是不安全的,不然也不会再创建一个多线程版的懒汉模式实现单例模式,那么以上代码在哪里会涉及到线程安全问题呢?这里出现问题的核心代码就是 getInstance 方法,下面通过画图的方式来对这里的线程安全问题进行讲解:

        如上图所示,在线程 t1 判断完成,当前是第一次执行 getInstance 方法后进入 if 语句内,没等创建实例就被调度走去执行线程 t2 ,此时 t2 虽然是第二次调用 getInstance 方法,但是由于线程 t1 调用 getInstance 方法还没有创建实例,所以线程 t2 执行 if 语句显示 instance 仍然为 null,此时线程 t2 开始创建实例,并返回实例,然后又跳转回线程 t1 ,t1 继续执行创建实例,这时,该进程中就会出现两个实例,也就出现了线程安全问题。  

        如何改进单线程的懒汉模式,使它也成为线程安全的代码呢?这就需要我们进行加锁操作,想要使这里的代码执行正确,其实只需把 if 和 创建实例的两个操作打包成原子的(不可拆分),这样就可以解决单线程的懒汉模式中的线程安全的问题,加锁逻辑如下图所示:

        如上图两个线程在加锁后的执行流程所示,此时就可以确保,一定是 t1 执行完实例(new)操作修改了 instance 之后再回到 t2 执行 if 语句了,这时 if 的条件就不会成立了,t2 就会直接返回 instance 了。

        但是这样加锁之后还有一个问题,如果 instance 已经创建过实例了,此时后续再调用 getInstance 方法就都是直接返回 instance 实例了,这时调用 getInstance 方法就属于纯粹的读操作了,就不会有线程安全问题了,不过,按照上图中的代码逻辑,即使创建完 instance 实例后是线程安全的代码,仍然每次调用都会先加锁再释放锁,此时效率就会变低(加锁意味着产生阻塞,一旦阻塞解除时间就不确定了)。

        为了解决上述加锁引入的新问题,我们可以在每次加锁前再进行一次判断,仍然是判断当前 instance 的值是否为 null ,为 null 就继续加锁,不为 null 就可以直接返回 instance 对象,不用再进行加锁操作了,具体代码如下图所示:

        如上图所示的代码中,synchronized 上下两条 if 语句中判断的内容是一样的,这里虽然 if 中进行的判断相同,但是所判断的含义还是有所差别:

  1. 第一个 if 判断当前是否要加锁;
  2. 第二个 if 判断的是当前是否要创建实例 

        上面代码很凑巧的 if 中的判断条件相同了,但是一个是为了保证“线程安全”一个是“保证“执行效率”,这也就形成了双重校验锁。

        代码改到此处,还是存在一个问题,那就是由指令重排序引起的线程安全问题,指令重排序是一种编译器的优化方式,调整原有的代码执行顺序,保证逻辑不变的前提下提高程序的效率,但是在多线程中,这种优化就很可能带来线程安全问题,上面代码中,创建 instance 实例的过程就很可能会被指令重排序,创建 instance 实例代码如下:

instance = new SingletonLazy();

        上面这段代码,可以拆分成三个大的步骤:

  1. 申请一段内存空间;
  2. 在这个内存空间上调用构造方法,创建出这个实例;
  3. 把这个内存地址赋值给 instance 引用变量。 

        正常的情况下,会按 1,2,3 的顺序来执行上面这段代码,但是编译器可能会将上面代码优化成 1,3,2 的顺序来执行,这时就可能会出现问题,如下图所示的情况: 

        如上图的线程调度过程,t2 线程执行完 getInstance 方法后得到的是一个各个属性都未初始化“全0”值的 instance 实例,此时如果使用 t2 线程如果使用了 instance 里面的属性或者方法就会出现错误。

        这种错误出现的原因是由于线程 t1 在创建实例执行完了 1,3 后,被调度走,此时 instance 指向的是一个非 null 的,但是未初始化的对象,这时 t2 线程就会判定 instance==null 不成立,直接 return ,得到一个各个属性都未初始“全0”值的 instance 实例,此时使用这个实例就会出现问题,但是如果创建实例的代码按照 1,2,3 的顺序来执行,就不会出现上述的问题了,所以解决这个问题的方法就是阻止编译器对这段代码的指令重排序,这就需要使用到我们前面文章介绍的关键字 volatile 了。

        这里还是再介绍一下 volatile 关键字的功能把,主要有两个:

  1. 保证内存可见性:每次访问变量都必须要重新读取内存,而不会优化成到寄存器或缓存中读取变量;
  2. 禁止指令重排序:针对这个 volatile 关键字修饰的变量的读写操作相关指令是不能被重排序的。 

        代码中需要进行指令重排序的地方是为 instance 创建实例的时候,所以我们可以直接针对这个变量加上 volatile 关键字进行修饰,这样,针对这个变量再进行读写操作就不会出现重排序了,此时,创建实例的顺序一定是 1,2,3 也就预防了上述的问题。

        代码修改到这里就算没有问题了,那么正确懒汉模式实现单例模式多线程版的代码就可以写出来了,代码及一些详细注释如下所示:

// 懒汉模式---多线程版
class SingletonLazy{
    // 这个引用指向唯一实例,初始化为 null,而不是立即创建实例
    private volatile static SingletonLazy instance = null;
    private static Object locker = new Object();
    private SingletonLazy() {}
    public static SingletonLazy getInstance() {
        // 如果 instance 为 null, 说明是首次调用,首次调用就需要考虑线程安全问题,需要加锁
        if (instance == null) {
            synchronized (locker) {
                if (instance == null){
                    // 首次调用 getInstance 方法,创建实例
                    instance = new SingletonLazy();
                }
            }
        }
        // 如果不是第一次调用 getInstance 方法,直接返回之前创建好的实例
        return instance;
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        // 利用"懒汉模式"实现的单例模式创建两个对象,观察这两个对象是否相同
        SingletonLazy b = SingletonLazy.getInstance();
        SingletonLazy b2 = SingletonLazy.getInstance();
        System.out.println(b==b2);
    }
}

·结尾

        文章到这里就要结束了,本篇文章利用前面文章介绍的多线程基础知识来实现了一个小案例——单例模式的实现,这里介绍的两种实现方式:饿汉模式与懒汉模式,由于饿汉模式从类加载时就已经创建好实例,后续获取实例都是读操作不涉及线程安全问题,所以饿汉模式下的单例模式代码天生就是线程安全的,反观,懒汉模式在多线程与单线程下就有很大的差别了,此时单线程版的懒汉模式在多线程中就会引发线程安全问题,上面文章详细介绍了每个会出现线程安全问题的地方,希望能够给大家讲解清楚,最后在基于单线程版的懒汉模式代码下,修改出了多线程版的懒汉模式代码,理解清楚这里相信会对你理解线程安全问题有很大的帮助,如果对文章哪里感到疑惑,欢迎在评论区进行留言讨论哦~我们下一篇文章再见~~~

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

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

相关文章

扩散模型对抗蒸馏:ADD 和 Latent-ADD

扩散模型对抗蒸馏:ADD 和 Latent-ADD ADD(Adversarial Diffusion Distillation)和 Latent-ADD 是 StabilityAI 公司提出的一系列针对 Stable Diffusion 的扩散模型对抗蒸馏方法,通过对抗训练和蒸馏训练来提高扩散模型的采样速度&…

python基于图片内容识别的微信自动发送信息(对其中逻辑修改一些可以改为自动化回复)

1.内容基于python日常生活问题帮助 2.主要框架 import time from datetime import datetimeimport pyperclip import win32api import win32con import os import refrom Image_Content_Text_Recognition import ICTR from screenshot import img 上面是逻辑部分主要框架 i…

【开源免费】基于SpringBoot+Vue.JS在线视频教育平台(JAVA毕业设计)

本文项目编号 T 027 ,文末自助获取源码 \color{red}{T027,文末自助获取源码} T027,文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 新…

解析 Vue 模板的本质:从语法糖到渲染过程

大家耳熟能详的表述如下:Vue 模板的本质其实是一种 声明式渲染 的形式,它在开发过程中提供了将组件的结构与逻辑分离的便利。 也就是说,模板 template 的存在只是为了让我们以更直观的方式描述界面的结构,然而在运行时&#xff0…

Android Framework AMS(09)service组件分析-3(bindService和unbindService关键流程分析)

该系列文章总纲链接:专题总纲目录 Android Framework 总纲 本章关键点总结 & 说明: 说明:上上一章节主要解读应用层service组件启动的2种方式startService和bindService,以及从APP层到AMS调用之间的打通。上一章节我们关注了s…

北京大学冯惠:与卓越者同行,方能更快的成长 | OceanBase数据库大赛获奖选手访谈

本文邀请2022 OceanBase 数据库大赛的季军,来自北京大学的冯惠同学,与我们分享如何寻找自己的兴趣;在一番经历后,对于产品与研发的职业方向观察;以及如何在学生时期提升个人专业能力,和参加数据库大赛的个人…

【Python技术】利用akshare定时获取股票实时价,低于5日线钉钉通知报警

今天看了下大盘,临时有个想法,我想知道某个股票回踩5日线的价格,如果实时价格低于5日线通过钉钉报警通知我。 说干就干,临时撸了下简单的代码,仅做演示。 1、计算5日线思路 很多券商软件的MA5价格是近5个交易日收盘…

Java项目-基于springboot框架的医患档案管理系统项目实战(附源码+文档)

作者:计算机学长阿伟 开发技术:SpringBoot、SSM、Vue、MySQL、ElementUI等,“文末源码”。 开发运行环境 开发语言:Java数据库:MySQL技术:SpringBoot、Vue、Mybaits Plus、ELementUI工具:IDEA/…

Hi3061M——VL53L0X激光测距(IIC)(同样适用于其他MCU)2

目录 前言资源下载移植基本使用IO配置调用测量 总结 前言 昨晚太晚了,草草结束了上一篇,今天更新下半部分。 昨天已经讲了VL53L0X的使用流程,无非就是进行6步的效准初始化,然后配置下模式和时间,开始采样,…

LDAP 部署手册

Centos 1. 安装openldap软件 # 安装openldap yum -y install openldap compat-openldap openldap-clients openldap-servers openldap-servers-sql openldap-devel migrationtoolscp /usr/share/openldap-servers/DB_CONFIG.example /var/lib/ldap/DB_CONFIG chown ldap:ldap…

Leetcode 跳跃游戏 二

核心任务是找出从数组的起点跳到终点所需的最小跳跃次数。 这段代码解决的是“跳跃游戏 II”(Leetcode第45题),其核心任务是找出从数组的起点跳到终点所需的最小跳跃次数。 class Solution {public int jump(int[] nums) {//首先处理特殊情…

“智驭医疗·未来已来“:医疗保健知识中台的搭建与应用

前言 随着科技的飞速发展,医疗保健领域正在经历深刻的变革。知识中台作为促进医疗行业应用智能化升级的关键底座,正在逐渐成为提高医疗服务质量和效率的重要工具。本文将探讨医疗保健知识中台的内容构成、应用案例以及更新与维护机制。 一、医疗保健知识…

基于ASP.NET的小型超市商品管理系统

文章目录 前言项目介绍技术介绍功能介绍核心代码数据库参考 系统效果图 前言 示 文章底部名片,获取项目的完整演示视频,免费解答技术疑问 项目介绍 小型超市商品管理系统是一款针对小型超市日常运营需求设计的软件解决方案。该系统主要内容有商品类别…

【JS】无法阻止屏幕滚动

监听滚轮事件,阻止默认行为,但未生效,且控制台报错。 window.addEventListener(wheel, (e) > {e.preventDefault(); })这是因为现代浏览器使用 Passive 事件监听器,默认启用了 passive 模式以确保性能,不会调用 pr…

【软件安装与配置】Redis for Windows

1. 下载 Redis Redis 官方没有直接支持 Windows 的安装程序,但可以使用第三方的 Windows 版本。推荐使用 Memurai 或从 Microsoft archive 提供的 Redis for Windows 下载。 2. 安装 Redis 下载适合 Windows 的安装包,本文以Microsoft archive安装包为…

Git_IDEA集成Git

Git_IDEA集成Git 配置 Git 忽略文件 创建忽略规则文件 引用忽略配置文件 定位 Git 程序 初始化本地库 添加到暂存区 提交到本地库 切换版本 创建分支 切换分支 合并分支 解决冲突 配置 Git 忽略文件 创建忽略规则文件 引用忽略配置文件 在 .gitconfig 文件中进行&…

[Git]一文速通

概述 Git是一个分布式版本控制工具,主要用于管理开发过程中的源代码文件(Java类、xml文件、html页面等, )在软件开发过程中被广泛使用 Git的作用 代码回溯版本切换多人协作远程备份 通过Git 仓库来存储和管理代码 文件,Git 仓库分为两种: 本地仓库: 开…

C++和OpenGL实现3D游戏编程【连载15】——着色器初步

🔥C和OpenGL实现3D游戏编程【目录】 1、本节实现的内容 上一节我们介绍了通过VBO、VAO和EBO怎样将顶点发送到GPU显存,利用GPU与显存之间的高效处理速度,来提高我们的图形渲染效率。那么在此过程中,我们又可以通过着色器&#xff…

webstorm 编辑器配置及配置迁移

1.下载地址 WebStorm:JetBrains 出品的 JavaScript 和 TypeScript IDE 其他版本下载地址 2.安装 点击下一步安装,可根据需要是否删除已有版本 注意: 完成安装后需要激活 3.设置快捷键 以下为个人常用可跳过或根据需要设置 如&#xff1a…

字幕怎么自动生成?教你5种视频加字幕方法

在这个短视频时代,视频内容已成为传播信息、娱乐大众的重要载体。而字幕作为视频不可或缺的一部分,不仅能够提升观众的观看体验,还能跨越语言障碍,让所有观众都能享受视频的魅力。但怎么给视频加上字幕呢?下面给大家分…