【单例模式】保证线程安全实现单例模式

news2024/12/24 1:20:22

📄前言:本文是对经典设计模式之一——单例模式的介绍并讨论单例模式的具体实现方法。


文章目录

  • 一. 什么是单例模式
  • 二. 实现单例模式
    • 1. 饿汉式
    • 2. 懒汉式
      • 2.1 懒汉式实现单例模式的优化(一)
      • 2.2 懒汉式实现单例模式的优化(二)
    • 3. 饿汉式和懒汉式的对比

一. 什么是单例模式

以下单例模式的概念:

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

“说人话”版本:单例模式是指某个类在程序运行过程中当且仅当会被实例出一个对象的设计模式。

为什么要使用单例模式?
在一个程序中,若多个地方都需要用到一个类的某些方法且这些方法实现的功能完全一样时,如果实例化出多个对象,会造成内存空间的浪费,占用系统资源。
例如:当我们在Java程序中需要进行数据库操作时,首先需要获得一个数据源(DataSource)来确定数据库的唯一网络资源位置,要进行数据库操作只需通过同一个数据源建立连接,在这个场景下 数据源对象 只需要一个,从而避免了系统资源的浪费。
在这里插入图片描述


二. 实现单例模式

实现单例模式有以下两个关键点:

  1. 单例模式下类只能有一个实例化的对象,因此该类不能通过构造方法任意实例化,其构造方法应该私有化
  2. 想获得该类的实例对象,可以通过类的静态方法来获取。

单例模式按实现的方式可以分为以下两种:

  • 饿汉式:在类加载时就创建出对象
  • 懒汉式:在获取对象实例时才创建对象(使用时)

1. 饿汉式

饿汉即形容一个人在肚子饥饿时便一次性把自己吃撑,后续便不再进食。饿汉式实现单例模式即使一个类在程序的类加载的阶段便创建出对象,后续程序中想使用该对象就可以直接获取。(这里可以简单理解为程序启动后类就会被实例化)

饿汉式实现单例模式可将代码实现分为以下几步:
1.定义一个由私有的、不可修改的、静态的类属性并进行实例化。
2.将构造方法私有化
3.定义一个方法,使类属性可以被获取。

具体的代码实现如下:

class SingleTon1 {
    //饿汉模式,即在类加载时就实例化出对象
    private final static SingleTon1 instance = new SingleTon1();
    
    // 使构造方法私有化,保证类的实例只能被创建一个
    private SingleTon1() {}
	
	// 通过静态方法获取类对象
    public static SingleTon1 getInstance() {
        return instance;
    }
}

2. 懒汉式

懒汉即形容一个人在饥饿时才选择进食且不一次性吃饱,等待后续饥饿便再次进食。懒汉式实现单例模式即在第一次调用方法获取类的实例对象时才进行创建,后续程序中想使用该对象就可以直接获取。

懒汉式实现单例模式可将代码实现分为以下几步:
1.声明一个私有的、静态的类属性。
2.将构造方法私有化
3.定义一个方法,使类属性可以被获取;当该方法被调用时,判断类属性的值并决定是否进行类的实例化。

具体的代码实现如下:

class SingleTon2 {
 
    private static SingleTon2 instance;
	
	// 使构造方法私有化,保证类的实例只能被创建一个
    private SingleTon2() {}

	// 判断是否存在实例对象,没有则创建对象并放回
    public static SingleTon2 getInstance() {
        if(instance == null) {
        	instance = new SingleTon2();
        }
        return instance;
    }
}

在饿汉式创建单例对象的基础上,我们只做出了微小的改动便实现了懒汉式单例模式。那么上面的代码是否就是正确的呢?
答案是:不完全正确。因为上述代码在单线程环境中运行没有问题,但在多线程的环境下就可能出现“错误”,导致理想中的单例模式被打破

下面模拟在多线程环境下使用上述懒汉模式代码获取实例对象,程序中用一个静态成员变量 count 来记录类被实例化的次数

class SingleTon3 {

    public static int count;

    private static SingleTon3 instance;

    // 使构造方法私有化,保证类的实例只能被创建一个
    private SingleTon3() {}

    // 判断是否存在实例对象,没有则创建对象并返回
    public static SingleTon3 getInstance() {
        if(instance == null) {
            instance = new SingleTon3();
            count++;
        }
        return instance;
    }
}

public class Demo25 {

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                SingleTon3 instance = SingleTon3.getInstance();
            });
            threads[i].start();
        }
        
        // 等待所有线程执行完毕
        for (int i = 0; i < 10; i++) {
            threads[i].join();
        }
        // 获取类的实例化次数
        System.out.println(SingleTon3.count);
    }

}

代码的可能结果如下:(因为多线程的抢占式执行,每次的执行结果可能并不相同)
在这里插入图片描述

2.1 懒汉式实现单例模式的优化(一)

为什么会出现上述现象呢?饿汉式实现单例模式是否也会出现这种现象?
最根本的原因是:在多线程环境下对一个共享的数据进行了修改操作。当 instance 还未被实例化时,因为线程的抢占式执行,导致出现了多个线程同时执行到了 if 条件的判断,这些线程都认为 instance 未被实例化,因此各自初始化了一个类对象,造成了单例模式被打破。(执行情况如下图)
通过以上分析,我们很容易知道通过饿汉式的实现方式并不会出现“单例模式被破坏”的现象,因为他的类属性在类加载时便已初始化完毕,且获取该属性时并不涉及修改操作,因此饿汉式保证了在单线程或多线程下的绝对安全。
在这里插入图片描述

如何防止这种情况的发生呢?
在多线程的场景中,毫无疑问使用 synchronized 对修改操作进行加锁是其中的一个解决办法。

如何进行有效加锁?
由上图可以知道,导致出现类被多次实例的原因在于 if 语句的判断出现错误,因此想要进行有效加锁,需要每个未获取锁的线程在进行 if 语句的判断前进入阻塞状态,等待第一个获取锁的线程示例出一个类对象时,其他的线程才可进行 类属性是否为空的判断。(代码如下)

class SingleTon2 {

    private static SingleTon2 instance;
    
    private SingleTon2() {}
    
    public static SingleTon2 getInstance() {
    	// 对 if 条件判断语句进行加锁操作
         synchronized (SingleTon2.class) {
             if(instance == null) {
                 instance = new SingleTon2();
             }
         }
        return instance;
    }
}

上述代码实际上已经能够保证多线程下的安全问题,可初始化了类对象后,后续对 if条件的判断 其实已经失去了加锁的必要性,因为类属性已被实例化,多余的加锁操作会增加系统的开销,增加程序的运行时间。
因此,我们需要对是否进行加锁再进行一次判断。(修改代码如下)

private static volatile SingleTon2 instance;
	private static SingleTon2 instance;
	
    private SingleTon2() {}
    
    public static SingleTon2 getInstance() {
        // 第一次对象实例化后,后续并不涉及 修改操作,无需重复加锁判断
        if(instance == null) {
            // 在多线程 并发执行下,防止 创建多个实例
            synchronized (SingleTon2.class) {
                if(instance == null) {
                    instance = new SingleTon2();
                }
            }
        }
        return instance;
    }

2.2 懒汉式实现单例模式的优化(二)

上述已经完美解决了类属性被多次实例化的线程安全问题,但其实还存在另一个潜在的安全问题:因 new() 操作触发的指令重排序造成的多线程安全问题。
什么是指令重排序?
JVM 在保证最终代码执行逻辑不变的情况下,对某一段指令的执行顺序做出了调整,从而提高了程序的执行效率。

new()操作实际会被拆分为以下3步:
1.申请一块内存空间
2.在内存空间上利用构造方法构造对象
3.把对象在内存中的地址赋值给 instance 引用

当第一个线程调用静态方法获取类属性时,因 new()操作触发了指令重排序,先执行了第1、3步操作,此时 instance引用不为空,但还未对对象的属性和方法进行初始化。若此时后续的线程经过 if 判断后得到了 instance 引用,并使用了这个还没初始化的非法对象的属性或方法时,就可能出现不可预期的错误。

因此,instance 属性需要用 volatile 关键字来禁止指令重排序。(代码如下)

class SingleTon2 {
    // 禁止指令重排序, 防止未实例完成的对象里的属性 被非法使用
    private static volatile SingleTon2 instance;
    
    private SingleTon2() {}
    
    public static SingleTon2 getInstance() {
        // 第一次对象实例化后,后续并不涉及 修改操作,无需重复加锁判断
        if(instance == null) {
            // 在多线程 并发执行下,防止 创建多个实例
            synchronized (SingleTon2.class) {
                if(instance == null) {
                    instance = new SingleTon2();
                }
            }
        }
        return instance;
    }
}

3. 饿汉式和懒汉式的对比

  1. 饿汉式在程序启动后的类加载阶段就创建出类对象,能够直接使用实例对象;懒汉式在使用时才加载。
  2. 饿汉式不存在多线程安全问题;懒汉式可能存在多线程安全问题,需要对代码实现进行优化。
  3. 对内存要求不高的场景中可以直接使用饿汉式写法;对内存要求高的场景下,可以使用懒汉式写法,在需要使用时才创建对象。

以上就是本篇文章的全部内容了,如果这篇文章对你有些许帮助,你的点赞、收藏和评论就是对我最大的支持。
另外,文章可能存在许多不足之处,也希望你可以给我一点小小的建议,我会努力检查并改进。

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

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

相关文章

EI论文复现:考虑冷热运行特性的综合能源系统多时间尺度优化调度程序代码!

适用平台/参考文献&#xff1a;MatlabYalmipCplex&#xff1b; 参考文献&#xff1a;电力系统自动化《含冰蓄冷空调的冷热电联供型微网多时间尺度优化调度》 提出考虑冷热特性的综合能源系统多时间尺度优化调度模型&#xff0c;日前计划中通过多场景描述可再生能源的不确定性…

表白墙网站PHP源码,支持封装成APP

源码介绍 PHP表白墙网站源码&#xff0c;适用于校园内或校区间使用&#xff0c;同时支持封装成APP。告别使用QQ空间的表白墙。 简单安装&#xff0c;只需PHP版本5.6以上即可。 通过上传程序进行安装&#xff0c;并设置账号密码&#xff0c;登录后台后切换模板&#xff0c;适配…

牛客30道题解析精修版

1.异常处理 都是Throwable的子类&#xff1a; ① Exception&#xff08;异常&#xff09;:是程序本身可以处理的异常。 ② Error&#xff08;错误&#xff09;: 是程序无法处理的错误。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时&#xff0c;一般不需要…

《统计学习方法:李航》笔记 从原理到实现(基于python)-- 第3章 k邻近邻法

文章目录 第3章 k邻近邻法3.1 k近邻算法3.2 k近邻模型3.2.1 模型3.2.2 距离度量3.2.3 k值的选择3.2.4 分类决策规则 3.3 k近邻法的实现&#xff1a;kd树3.3.1 构造kd树3.3.2 搜索kd树 算法实现课本例3.1iris数据集scikit-learn实例kd树:构造平衡kd树算法例3.2 《统计学习方法&a…

把Windows系统装进U盘,到哪都有属于你自己的电脑系统

前言 自从接触到WinPE启动盘之后&#xff0c;小白就突然萌生了一个想法&#xff1a;为啥不能把完整的Windows放进U盘呢&#xff1f; 实际上Windows是可以安装进U盘的&#xff0c;外出的时候带上&#xff0c;只需要有台正常开机的电脑就可以使用属于自己的系统。 这个是早已经…

一分钟教你搭建《幻兽帕鲁》服务器

幻兽帕鲁是一款由Pocketpair开发的开放世界生存游戏&#xff0c;融合了多种玩法的游戏&#xff0c;其独特的题材和画风吸引了很多玩家&#xff0c;越来越多的玩家开始尝试自己搭建服务器&#xff0c;享受更加自由的游戏体验。本文将为大家详细介绍如何从零开始搭建《幻兽帕鲁》…

「 网络安全常用术语解读 」杀链Kill Chain详解

1. 简介 早在2009年&#xff0c;Lockheed Martin公司就提出了杀链(Kill Chain)理论&#xff0c;现在也称之为攻击者杀链(Attacker Kill Chain)。杀链其实就是攻击者进行网络攻击时所采取的步骤。杀链模型包括7个步骤&#xff1a;1侦察 -> 2武器化 -> 3交付 -> 4利用 …

java程序判等问题

注意 equals 和 的区别 对基本类型&#xff0c;比如 int、long&#xff0c;进行判等&#xff0c;只能使用 &#xff0c;比较的是直接值。因为基本类型的值就是其数值。对引用类型&#xff0c;比如 Integer、Long 和 String&#xff0c;进行判等&#xff0c;需要使用 equals 进…

【立创EDA-PCB设计基础】6.布线铺铜实战及细节详解

前言&#xff1a;本文进行布线铺铜实战及详解布线铺铜的细节 在本专栏中【立创EDA-PCB设计基础】前面完成了布线铺铜前的设计规则的设置&#xff0c;接下来进行布线 布局原则是模块化布局&#xff08;优先布局好确定位置的器件&#xff0c;例如排针、接口、主控芯片&#xff…

Sulfo Cy3 hydrazide,磺化-Cy3-酰肼,可用于与生物分子的羰基衍生物偶联

您好&#xff0c;欢迎来到新研之家 文章关键词&#xff1a;Sulfo-Cyanine3-hydrazide&#xff0c;Sulfo Cy3 hydrazide&#xff0c;Sulfo Cyanine3 HZ&#xff0c;磺化 Cy3 酰肼&#xff0c;磺化-Cy3-酰肼 一、基本信息 产品简介&#xff1a;Sulfo-Cyanine3-hydrazide能够与…

远程连接银河麒麟

目录 一、防火墙服务 二、安装SSH服务 1.验证SSH服务是否安装 2.安装SSH服务 三、启动SSH服务 四、远程连接 1.切换登录用户 2.查看IP地址 3.FinalShell连接 4.切换root用户 前言: 本篇主要讲述在Win10系统中通过FinalShell远程连接银河麒麟桌面操作系统V10 一、防火…

android studio从空白开始

对我来说&#xff0c;真正的第一步是清理电脑C盘。从剩余8G清理到25G&#xff0c;把原来看不顺眼又不敢删的文件夹和软件全删了&#xff0c;删爽了的后果就是&#xff0c;用两天的时间在把一些环境配置慢慢装回来&#xff0c;node.js&#xff0c;jdk&#xff0c;npm。努力把它们…

零基础学习【Mybatis Plus】这一篇就够了

学习目录 1. 快速入门1-1. 常用注解总结 1-2. 常用配置 2. 核心功能3. 扩展功能4. 插件功能 1. 快速入门 1-1. 常用注解 MybatisPlus中比较常用的几个注解如下&#xff1a; TableName: 用来指定表名Tableld: 用来指定表中的主键字段信息TableField: 用来指定表中的普通字段信…

Excel·VBA时间范围筛选及批量删除整行

看到一个帖子《excel吧-筛选开始时间&#xff0c;结束时间范围内的所有记录》&#xff0c;根据条件表中的开始时间和结束时间构成的时间范围&#xff0c;对数据表中的开始时间和结束时间范围内的数据进行筛选 目录 批量删除整行&#xff0c;整体删除批量删除整行&#xff0c;分…

[每日一题] 01.24 - 求三角形

求三角形 n int(input()) count1 (1 n) * n // 2 count2 n * n lis1 [str(i).zfill(2) for i in range(1,count1 1)] lis2 [str(i).zfill(2) for i in range(1,count2 1)]for i in range(0,len(lis2),n):print(.join(lis2[i:i n]))print()for i in range(1,n 1):tem…

Vue+Element(el-upload+el-form的使用)+springboot

目录 1、编写模板 2、发请求调接口 3、后端返回数据 1.编写实体类 2.Controller类 3、interface接口&#xff08;Service层接口&#xff09; 4.Service&#xff08;接口实现&#xff09; 5、interface接口&#xff08;Mapper层接口&#xff09; 6、xml 4、upload相关参…

微信小程序之全局配置-window和tabBar

学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持&#xff0c;想组团高效学习… 想写博客但无从下手&#xff0c;急需…

抖捧AI实景自动直播怎么玩

​在如今的全民直播时代&#xff0c;直播已经成为了众多实体店、品牌方所刚需的技能&#xff0c;但是大多数都不具备太多的直播能力 &#xff0c;这个时候实景自动直播就应运而生&#xff0c;但是很多人都没有想清楚&#xff0c;AI实景自动直播&#xff0c;到底适不适合自己用呢…

使用官方标定工具Dynamic Calibrator对RealSense D435i进行标定(二)

工具的安装教程可以看我的上一篇博文&#xff1a;Ubuntu 18.04安装Dynamic Calibration software for the Intel RealSense™ D400 Series Cameras&#xff08;一&#xff09; 使用教程参考user guide&#xff1a;https://www.intel.com/content/www/us/en/support/articles/0…

2023春秋杯冬季赛 --- Crypto wp

文章目录 前言Cryptonot_wiener 前言 比赛没打&#xff0c;赛后随便做一下题目 Crypto not_wiener task.py: from Crypto.Util.number import * from gmpy2 import * import random, os from hashlib import sha1 from random import randrange flagb x bytes_to_long(f…