《javaEE篇》--单例模式详解

news2024/12/29 8:26:28

目录

单例模式

饿汉模式

懒汉模式

懒汉模式(优化) 

指令重排序

 总结

单例模式

单例模式属于一种设计模式,设计模式就好比是一种固定代码套路类似于棋谱,是由前人总结并且记录下来我们可以直接使用的代码设计思路。

单例模式就是,在有些场景中希望一个类只能有一个对象,不能有多个,这时你可能会觉得,这还不简单,我保证自己只new一个对象不就好了,但是你能保证自己只new一个对象,但是你能保证别人不会new对象吗?又或者,你真的能确保自己只new一个对象吗?所以“我保证自己只new一个对象不就好了”,这只是一个君子协定,光靠人是非常不靠谱的,所以我们要依靠计算机来帮助我们实现这个协定。

//这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个.

单例模式的实现方式可以分为两种,饿汉模式懒汉模式

饿汉模式

具体实现方法

  1. 用static修饰,在类的内部创建一个现成的实例,让对象在在类加载时就被创建
  2. 提供一个方法当需要这个对象时就通过这个方法获得
  3. 用private修饰构造方法,这样外界就不能实例化这个类了

通过这三个方法就可以保证只有一个该类对象了

class Singleton{//创建时时机比较早(饿汉模式)
    //当类被加载时就会执行这里的创建实例操作
    private static Singleton instance = new Singleton();
    //后续需要这个对象,都通过这个方法获取
    public static Singleton getInstance(){
        return instance;
    }
    //私有构造方法
    private Singleton(){

    }
}
public class Demo17 {
    public static void main(String[] args) {
        //把构造方法设置为私有,之后就无法实例化这个对象了,这能通过刚刚创建的方法
        //Singleton singleton = new Singleton();
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //这两个对象其实是一样的
        System.out.println(s1 == s2);
    }
}

 通过运行结果我们可以验证,上述在main方法中创建的对象是同一个,也就是刚刚在类中创建的那一个对象

这种方法会在类加载时就把对象创建好,如果不使用就会造成资源浪费

懒汉模式

具体实现

  1. 用static修饰,只声明类型创建实际对象
  2. 私有构造方法
  3. 提供外界用来获得对象的方法,设定当第一次调用方法时才创建对象
class SinngletonLazy{//在第一次调用时创建对象(懒汉模式)
    //只声明一个类型不创建实际对象
    private static SinngletonLazy instance = null;
    public static SinngletonLazy getInstance(){
        //如果是第一次调用,就创建实例对象,否则直接返回对象
            if(instance == null){
                instance = new SinngletonLazy();
            }
        return instance;
    }
    //私有构造方法
    private SinngletonLazy(){
    }
}

 饿汉模式和懒汉模式的最大区别就是,饿汉模式在类加载时就创建了对象,懒汉模式在第一次调用方法时才创建对象,假如我们要打开一本电子书,饿汉模式是要把整本书都加载完成才可以看,而懒汉模式则是在翻页时才加载下一页,可想而知懒汉模式是更高效的。

懒汉模式(优化) 

不过上述实现懒汉模式的方法只在单线程下才适用,如果是在多线程环境下会引起线程安全问题。

这里我来画图解释一下

如果当t1执行到①时,if判定为真,程序将要进到if语句内部,此时t2线程插了进来,由于t1线程还没创建出对象,所以t2线程的if语句也会判定为真,接着t2线程new了一个对象然后返回,最后又回到t1线程这边紧接着又new了一个对象,此时代码就出现问题了。

我们第一时间想到的方法就是直接给方法加上锁,这个方法确实可以解决问题,但是这样的话,代码的并发性就降低了,进而影响到代码的效率,所以我们要换种思路。

可以思考一下,我们的加锁操作只是再第一次创建对象时才需要,所以我们在加锁操作前再加一个if语句来判断当前是否需要加锁,这样就既实现了懒汉模式,又优化了代码的效率

class SinngletonLazy{//在第一次调用时创建对象(懒汉模式)
    //只声明一个类型不创建实际对象
    private static SinngletonLazy instance = null;
    public static SinngletonLazy getInstance(){
        //如果是第一次调用,就加锁创建对象,否则直接返回对象
        if(instance == null) {//判断是否需要加锁
            synchronized (SinngletonLazy.class){
                if (instance == null) {//判断是否需要new对象
                    instance = new SinngletonLazy();
                }
            }
        }
        return instance;
    }
    //私有构造方法
    private SinngletonLazy(){
    }
}

 用简洁的话总结一下就是,因为为了防止资源浪费,所以使用一个if条件判断是否需要创建对象,因为在多线程下会出现线程安全的问题,所以要加锁,又因为只需要再第一次创建对象时才需要加锁,所以为了提升效率,再加一层if判断当前是否需要加锁(这两个if本质上没有什么关系,只是刚好判断条件一样而已)

这种方法也叫做Double Check(双重检验) + Lock(加锁) 

虽然当前的代码已经很完善了但是还是会有指令重排序的问题 

指令重排序

指令重排序也是一种编译器的优化,是编译器为了提高效率,在保证代码逻辑顺序不变的情况下,改变代码的实际顺序。

实际上这里的new操作可以分成三个步骤

  1. 向内存申请空间
  2. 在刚刚申请的空间上构造对象
  3. 把刚刚申请的空间的地址付给instance

 但是后面两个步骤对于编译器来说是可以颠倒的,按照123或者132来执行都是可以的,就看那种效率快,在单线程下是没有问题的但是在多线程下就会出现问题

假设有t1,t2线程,t1线程执行到new操作之后先执行1,3,此时编译器已经把刚刚申请的内存地址付给instance,Instance此时已经是一个非空的了,也就是说,此时instance指向一个还没有初始化的非法对象。但是这个时候还没有执行2操作,假如这时t2线程开始执行,判定第一个instance==Null,条件不成立(因为刚刚t1线程已经给instance赋值了),t2线程就直接会返回instance对象。但是此时,t1还没有到内存上构造出对象,T2线程的代码可能就会访问instance里面的属性了,进而就会引起一些bug。

 这里可能会有疑问了,刚刚不是给new操作加锁了吗?为什么还会再new操作时插入其他线程?这就是刚刚代码的缺陷之处,我们为了提高效率只是给new操作加锁了,但是此时t2线程只是执行到了第一个if条件,还没有涉及到任何锁操作,就更谈不上阻塞等待了。t2线程此时连第一个if条件都没有进去,就拿着一个还没有初始化的非法对象返回了。

解决方法

不过解决方法也很简单,使用volatile关键字修饰就可以了,volatile可以防止指令重排序,相当于告诉编译器,不要进行代码优化,让它按照本来的顺序执行

此外volatile还有一个作用:保证内存可见性,就是每一时刻线程读取到该变量的值都是内存中最新的那个值(线程每次操作该变量都需要先读取该变量)

 最终代码展示:

class SinngletonLazy{//在第一次调用时创建对象(懒汉模式)
    //只声明一个类型不创建实际对象
    private static volatile SinngletonLazy instance = null;
    public static SinngletonLazy getInstance(){
        //如果是第一次调用,就加锁创建对象,否则直接返回对象
        if(instance == null) {//判断是否需要加锁
            synchronized (SinngletonLazy.class){
                if (instance == null) {//判断是否需要new对象
                    instance = new SinngletonLazy();
                }
            }
        }
        return instance;
    }
    //私有构造方法
    private SinngletonLazy(){
    }
}

 总结

饿汉模式:在类加载时创建对象,通过方法直接返回该对象,不会出现并发安全问题

懒汉模式:在第一次需要对象是才会创建对象,但会有并发问题,建议使用Double Check(双重检验) + Lock(加锁) 可以很好的解决问题

为了在多线程环境下防止,疑问指令重排序而导致代码出现问题,要使用volatile修饰对象

以上就是博主对单例模式知识的分享,在之后的博客中会陆续分享有关线程的其他知识,如果有不懂的或者有其他见解的欢迎在下方评论或者私信博主,也希望多多支持博主之后和博客!!🥰🥰

下一篇博客博主将分享有关阻塞队列等知识,还希望多多支持一下!!!😊

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

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

相关文章

音视频入门基础:WAV专题(3)——FFmpeg源码中,判断某文件是否为WAV音频文件的实现

一、引言 通过FFmpeg命令: ./ffmpeg -i XXX.wav 可以判断出某个文件是否为WAV格式的音频文件: 所以FFmpeg是怎样判断出某个文件是否为WAV格式的音频文件呢?它内部其实是通过wav_probe函数来判断的。从文章《FFmpeg源码:av_prob…

02、爬虫数据解析-Re解析

数据解析的目的是不拿到页面的全部内容,只拿到部分我们想要的内容内容。 Re解析就是正则解析,效率高准确性高。学习本节内容前需要学会基础的正则表达式。 一、正则匹配规则 1、常用元字符 . 匹配除换行符以外的字符 \w 匹配字母或数字或下划…

软件测试---网络基础、HTTP

一、网络基础 (1)Web和网络知识 网络基础TCP/IP 使用HTTP协议访问Web WWW万维网的诞生 WWW万维网的构成 (2)IP协议 (3)可靠传输的TCP和三次握手策略 (4)域名解析服务DNS &#xff0…

一篇文章学完Python基础

1. 字符串 str1 "Hello" str2 " World" print(str1 str2) # 输出:HelloWorld 1.1 字符替换 text "Hello, World!" new_text text.replace("World", "Python") print(new_text) # 输出:…

大数据-52 Kafka 基础概念和基本架构 核心API介绍 应用场景等

点一下关注吧!!!非常感谢!!持续更新!!! 目前已经更新到了: Hadoop(已更完)HDFS(已更完)MapReduce(已更完&am…

橙单前端项目下载编译遇到的问题与解决

今天下载orange-admin前端项目,不过下载下来运行也出现一些问题。 1、运行出现下面一堆错误,如下: 2、对于下面这个错误 error Expected linebreaks to be LF but found CRLF linebreak-style 这就是eslint的报错了,可能是原作者…

全开源收银系统源码-支付通道

1.收银系统开发语言 核心开发语言: PHP、HTML5、Dart后台接口: PHP7.3后合管理网站: HTML5vue2.0element-uicssjs线下收银台(安卓/PC收银、安卓自助收银): Dart3框架:Flutter 3.19.6助手: uniapp商城: uniapp 2.支付通道 智慧新零售收银系统…

SQL语句(以MySQL为例)——单表、多表查询

笛卡尔积(或交叉连接): 笛卡尔乘积是一个数学运算。假设我有两个集合 X 和 Y,那么 X 和 Y 的笛卡尔积就是 X 和 Y 的所有可能组合,也就是第一个对象来自于 X,第二个对象来自于 Y 的所有可能。组合的个数即为两个集合中…

天机学堂第二天项目 添加我的课表 项目总结

目录 根据产品原型得到数据库表结构 RabbitMq监听 构造器注入 幂等 mybatisplus 分页查询的多种写法 在new page里面添加排序 查询条件中 用orderBydESC指定排序 ​编辑 链式编程中使用page指定排序 stream流 ​编辑 在网关中解析token 根据产品原型得到数据库表结构 根…

IDEA Maven使用HTTP代理,解决Could not transfer artifact org.xxx问题

文章目录 一、前言二、遇到问题三、分析问题四、HTTP代理五、重新编译验证 一、前言 遇到这个问题,有两种解决办法 IDEA Maven使用HTTP代理,解决Could not transfer artifact org.xxx问题IDEA Maven使用国内镜像,解决Could not transfer arti…

Matlab编程资源库(10)离散傅立叶变换

一、离散傅立叶变换算法简要 给定一个N点的离散信号序列x(n),其中n表示时刻,n 0, 1, 2, ..., N-1。 定义离散傅立叶变换的频域序列X(k),其中k表示频率,k 0, 1, 2, ..., N-1。 通过以下公式计算每个频率对应的复数值&#xff…

win11 备份mysql数据 卸载mysql 5 安装mysql 8详细教程(mysql的数据备份与卸载与安装)

如果是第一次安装mysql便可以直接看第三步 第一步:数据备份 我选择的是备份全部数据 以管理员身份运行cmd输入mysqldump -u root -p --all-databases --routines --triggers --single-transaction > C:\ProgramTools\MySql\backup_5.6.sql 其中:-u…

golang 文件

golang 文件 概念 文件是计算机系统中用于存储和管理的 数据集合,具有唯一的名称,存在于存储介质上,包含创建、修改等属性,通过文件系统进行组织,用户可进行读取、写入等操作 文件流 文件输入流(InputS…

3D打印:重塑模具制造业的创新引擎

在科技浪潮的推动下,3D打印技术正以前所未有的速度渗透到制造业的核心,尤其在模具制造领域,它正引领一场深刻的创新革命。该技术通过颠覆传统制造范式,显著优化了模具生产的复杂流程,实现了从设计到成品的一体化的高效…

Git操作快速入门:掌握代码版本控制的秘诀

文章目录 文章内容概述Git的安装和配置创建和克隆仓库基本操作添加和提交文件查看状态和日志分支管理远程仓库操作 常见的错误和解决方法错误1:合并冲突(Merge Conflict)错误2:丢失修改(Detached HEAD)错误…

win11查找句柄泄露

1.打开任务管理器,不会的网上搜 2.选择详细信息 3.注意了 比较坑的一点 win11上详细信息不会默认显示句柄数。(默认没有句柄那一列,妈的花了我好长时间找) 右键,点击选择列 选择句柄 下面的列表里就能看到进程使用…

CSS技巧专栏:一日一例 11 -纯CSS实现多彩渐变按钮系列特效

CSS技巧专栏:一日一例 11 -纯CSS实现多彩渐变按钮系列特效 本篇,推荐给你几个按钮,先看一下图片 本例图片 案例分析 这是一个系列的按钮,它们具有共同的特点: 底层按钮层,具有一个彩色的渐变边框,上层是依据hover效果需要,可以是渐变,可以时白色。 鼠标hover效果…

Spring Data Redis 实践以及可视化工具使用

目录 一、安装 Redis Win 环境安装 Linux 环境安装 Redis可视化管理工具 二、Spring Data Redis的使用 Spring Cache简介 Spring Cache常用注解 EnableCaching Cacheable CachePut CacheEvict Spring Cache使用步骤 Redis存储JSON格式数据 使用Redis连接池 Jedi…

使用WebSocket协议调用群发方法将消息返回客户端页面

目录 一.C/S架构: 二.Http协议与WebSocket协议的区别: 1.Http协议与WebSocket协议的区别: 2.WebSocket协议的使用场景: 三.项目实际操作: 1.导入依赖: 2.通过WebSocket实现页面与服务端保持长连接&a…

Python包管理工具pip

1、安装pip cmd管理员模式打开控制台 python -m pip install --upgrade pip 2、添加pip环境变量 pip 路径 C:\Users\1\AppData\Local\Programs\Python\Python312\Scripts