设计模式之单例模式(懒汉, 饿汉)

news2024/12/23 17:46:46

文章目录

  • 一. 单例模式概述
  • 二. 单例模式的实现
    • 1. 饿汉模式
    • 2. 懒汉模式

一. 单例模式概述

单例模式是一种常用的软件设计模式, 该模式的主要目的是确保某一个类在内存中只能有一个实例对象, 通过单例模式的方法创建的类在当前进程中只有一个实例对象.

常见的单例模式有两种:

  1. 饿汉式, 这里的 “饿” 意义表述不够清晰, 用 “急” 来表述意义更加容易联想一些, 饿汉式就是类一加载就创建对象, 给人一种 “特别急切” 的感觉.
  2. 懒汉式, 在使用时才去创建对象.

单例模式有 3 个特点:

  1. 单例类只有一个实例对象.
  2. 该单例对象必须由单例类自行创建.
  3. 单例类对外提供一个访问该单例的全局访问点.

我们知道static修饰的成员是属于类的, 也就是只有一份, 所以我们可以使用static修饰的成员变量保存实例对象的引用.

二. 单例模式的实现

1. 饿汉模式

单例模式中, 一个类只能有一个实例对象, 我们可以使用static成员变量来保存这个唯一实例对象的引用, 同时, 为了防止类被创建多个实例对象, 需要将类构造方法封装为private修饰的私有方法, 在使用该类时必须要得到该类的这个实例对象, 所以我们还需要创建一个对外公开的获取该唯一实例的方法getInstance.

class Singleton {
    //使用static变量存储实例对象,类加载时创建对象.
    private static Singleton intance = new Singleton();
    //对外提供公开的方法获取该类的唯一实例.
    public static Singleton getInstance() {
        return intance;
    }
    //封装构造方法,防止该类被实例出新的对象
    private Singleton() {}
}

单线程情况下不存在线程安全的问题, 我们需要考虑多线程情况下上面的代码是否存在线程安全问题, 重点要看读写情况下的线程安全问题, 上面的代码只有getInstance涉及到读操作, 所以上面的代码是线程安全的.

2. 懒汉模式

懒汉式与饿汉明显的区别就是对象创建的时机不同, 懒汉式要等到第一次使用实例对象的时候才会创建对象, 所以我们将上面的代码稍做修改写出下面的代码.

class SingletonLazy {
    //在使用的时候创建对象,这里不初始化实例对象
    private volatile static SingletonLazy instance = null;
    //对外提供公开的方法获取该类的唯一实例,如果没有就创建
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
	//封装构造方法,防止该类被实例出新的对象
    private SingletonLazy() {}
}

那么再来考虑上面的代码在多线程情况下是否有线程安全问题存在, 代码中的getInstance方法中存在两次读操作(一次比较一次返回)和一次写操作(修改intsance变量的值),第一次创建对象时(即instance=null)可能会存在多个线程进入判断语句, 此时就可能会创建出多个实例对象, 看下图理解:

img

所以上面写出来的代码可能会触发多次new操作, 显然就不是单例了, 也就是说上面写出来的懒汉式的代码是线程不安全的.

导致上面的线程安全问题的主要原因是读, 比较和写这三个操作不是原子的, 这就导致了t2读到的值可能是t1还没来得及写的(脏读), 所以我们只需要将代码中的读写操作加锁即可这里的问题, 得到下面的代码:

class SingletonLazy {
    private volatile static SingletonLazy instance = null;

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

    private SingletonLazy() {}
}

但代码写到这里还是有问题的, 上述的线程安全问题只出现在instance没有初始化的时候, 如果instance已经有了实例对象, 后续调用getlnstanceinstance的值一定是非空的, 因此就会直接触发return, 相当于一个是比较操作, 一个是返回操作, 这就都是读操作了, 此时就不涉及线程安全问题了.

而上面的代码, 不论instance是否已经初始化, 每次调用getlnstance, 都需要进行加锁操作, 要知道加锁操作是有开销的, 在instance已经有了实例对象以后就完全没有必要去加锁了, 所以代码还需要修改, 在加锁前需要先判断一下instance是否已经初始化, 如果还没初始化就进行加锁, 如果实例对象创建了, 就不加锁了.

调整后的获取实例对象的代码如下:

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

到这里其实还没完, 还要考虑编译器优化造成的内存可见性和指令重排序的问题, 上面的代码中是有判断操作的(instance==null), 假设有很多线程, 都去进行getlnstance, 就会进行频繁的判断, 这时候就会有被优化的风险, 可能只有第一次读才是真正读了内存, 后续都是读寄存器/cache中的值, 这时候instance值变化时, 线程就感知不到了, 所以这里的内存可见性问题需要解决.

还有要注意这里的instance = new Singleton();操作, 拆分成三个步骤就是:

  1. 申请内存空间.
  2. 调用构造方法, 把这个内存空间初始化成一个合理的对象.
  3. 把内存空间的地址赋值给instance引用.

正常情况下,是按照123这个顺序来执行的, 但你防不住编译器的优化操作, 也就是指令重排序问题, 编译器为了提高程序效率, 可能就调整了代码执行顺序, 123这个顺序就可能变成132, 如果是单线程,123和132没有本质区别, 但是多线程环境下, 就会有问题了;

假设t1是按照132的步骤执行的, t1执行到13之后, 执行2之前,被切出cpu了, 然后t2来执行;

t1执行完3之后, 再从t2角度来看, 此处的引用就非空了, 此时此刻, t2就相当于直接返回了instance引用并且可能会尝试使用引用中的属性, 但是由于t1中的2操作还没执行完呢, t2拿到的是非法的对象(还没构造完成的不完整的对象), 这就可能会造成一些问题.

为了解决上述问题需要使用关键字volatile修饰instance变量来防止编译器优化, 从而保证内存可见性, 禁止指令重排序, 完整代码如下:

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() {}
}

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

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

相关文章

制作系统安装(微软操作系统系统)

系统安装制作步骤 准备工具:笔记本电脑 8G以上u盘 镜像ISO文件 微软系统下载:https://msdn.itellyou.cn/ 复制连接到迅雷下载,进行系统镜像下载。 U盘制作工具下载和制作: 制作工具网站下载:http://rufus.ie/zh/ 准…

MySQL避免插入重复数据

新建一张测试表,有三个字段,自增主键id、创建了唯一索引的user_name、以及普通字段address。然后插入一条数据作为原始数据,如下所示 1、insert ignore into 基于索引字段数据进行判断,如果索引数据存在,那么忽略本…

CSS单行/多行文本溢出隐藏

前言 在日常开发展示页面,如果一段文本的数量过长,受制于元素宽度的因素,有可能不能完全显示,为了提高用户的使用体验,这个时候就需要我们把溢出的文本显示成省略号 对于文本的溢出,我们可以分成两种形式…

jetson nano安装远程桌面,Qt,pytorch,tensorflow,virtualenv等

文章目录基于jetPack版本4.6.1一.基础组件配置检查二.基础组件安装1.安装pip32.安装python-opencv与机器学习常用包3.安装pytorch方法1(失败)方法2(成功)4.安装tensorflow-gpu5.安装QT6.板载摄像头使用7.安装中文输入法8.安装截图…

网络爬虫入门到实战

简介 数据采集文章 开始 入门程序 环境准备 pip3 install beautifulsoup4 基本操作 from urllib.request import urlopen from bs4 import BeautifulSouphtml urlopen("http://www.baidu.com") # print(html.read()) (打印html完整内容) bsObj BeautifulSou…

服务熔断和服务降级

服务之间是可以相互调用的,如果底层的服务出现了问题,那么他的上层服务也就会出问题 为了解决分布式系统的雪崩问题,SpringCloud提供了Hystrix熔断器组件 服务降级 服务降级并不会直接返回错误,而是提供一个补救措施&#xff0c…

简洁明了的ReentrantReadWriteLock总结

👳我亲爱的各位大佬们好 ♨️本篇文章记录的为 ReentrantReadWriteLock 相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬🙉🙉🙉。 ♨️如果文章有什么需要改进的地方还请大佬不吝…

王爽汇编(第四版)实验八

文章目录前言一、题目二、分析1.初始时指令存储情况2.运行时指令存储情况总结前言本文是学习王爽老师《汇编语言》(第四版)第九章 实验8 分析一个奇怪的程序 时的相关代码及分析。一、题目分析程序,思考程序是否可以正确返回;运行后再思考,为…

胭脂茉莉点评推荐上海大学法院李本教授诗集《秋月曲》诗歌6首

胭脂茉莉(右)和李本教授(左)于2019年冬天在上海留影胭脂茉莉 ,女 ,江苏人,年少习诗,作家、诗人。评论及随笔见诸媒体及报刊 。诗歌先后被选编入海内外多种选本及刊物,主要…

【我的渲染技术进阶之旅】glfw库简单介绍

文章目录一、为啥去了解glfw?二、glfw相关资料三、glfw简单示例1. 引入 GLFW 头文件2. 初始化和终止 GLFW3. 设置错误回调4. 创建窗口和上下文5.使 OpenGL 上下文成为当前上下文6. 检查窗口关闭标志7.接收输入事件8.使用 OpenGL 渲染9. 读取定时器10.交换缓冲区11. 处理事件12…

【C++】了解设计模式,模拟实现栈和队列

文章目录一.设计模式二.stack的模拟实现三.queue的模拟实现四.了解deque五、题目练习一.设计模式 设计模式有很多种,根据设计模式的参考书 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面…

SpringBoot+VUE前后端分离项目学习笔记 - 【11 SpringBoot代码生成器_MybatisPlus】

引入mp生成器依赖 pom.xml <!-- 代码生成器 --> <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.5.1</version> </dependency> <dependency><grou…

RabbitMQ 单机安装-CentOS

RabbitMQ 单机安装-CentOS 官网查看RabbitMQ和对应的Erlang版本 进入 RabbitMQ 官网 &#xff0c;点击 顶上的 Get Started 点击Download Installation 点击左侧的Erlang Versions 查看对应版本 根据自己需要安装的RabbitMQ版本&#xff0c;找到需要Erlang的版本。 下…

什么是Spring Cloud?Spring Cloud介绍

简介Spring Cloud项目的官方网址&#xff1a;https://projects.spring.io/spring-cloud/Spring Cloud 并不是一个项目&#xff0c;而是一组项目的集合。在 Spring Cloud中包含了很多的子项目&#xff0c;每一个子项目都是一种微服务开发过程中遇到的问题的一种解决方案。它利用…

红中群内每日分享题目解析——第一天

目录 题目一&#xff1a;最后一次用的英雄 题目二&#xff1a;狗哥去哪 摩斯密码 题目三&#xff1a;黑丝白丝还有什么丝 题目一&#xff1a;最后一次用的英雄 ​​​​​​ 感谢我徒弟(不是 告知并解出此题 杭椒的博客_CSDN博客-网安经验分享,网络安全工具,CISP-PTE备考笔记…

音视频开发常用工具

目录 1.VLC播放器简介 1.1 VLC 播放器 1.2 VLC的功能列表 1.3 VLC播放网络串流 1.4 VLC作为流媒体服务器 2. MediaInfo简介 2.1 MediaInfo 2.1.1 获取多媒体文件信息 2.1.2 支持的格式 2.1.3 .查看方式 2.1.4 国际化 2.2 MediaInfo使用方法 2.3 MediaInfo参数说明 3…

将powershell、cmd和vscode终端的编码永久修改成utf-8

powershell修改方法 1、以管理员身份打开powershe New-Item $PROFILE -ItemType File -Force 2、打开C盘&#xff0c;找到我的文档中的WindowsPowerShell文件夹 3、编辑这个ps1文件&#xff08;默认是空的&#xff09;&#xff0c;加上以下代码 $OutputEncoding [console…

Handler的消息机制与消息延迟代码实现

Handler的作用 Handler消息机制在Android中的应用非常广泛&#xff0c;很多组件的底层实现都是靠Handler来完成的&#xff0c;所以掌握Handler消息机制的原理还是非常重要的。Handler的主要功能有两点&#xff1a; 1.它可以在不同的线程之间传递消息 我们都知道Andorid中规定…

menuconfig与Kconfig入门学习

概述 menuconfig是Linux平台用于管理代码工程、模块及功能的实用工具。 menuconfig的使用方式通常是在编译系统之前在系统源代码根目录下执行make menuconfig命令从而打开一个图形化配置界面&#xff0c;再通过对各项的值按需配置从而达到影响系统编译结果的目的。 Nuttx的m…

新建一个SpringMVC项目

步骤1:创建Maven项目 打开IDEA,创建一个新的web项目 步骤2:补全目录结构 因为使用骨架创建的项目结构不完整&#xff0c;需要手动补全 步骤3:导入jar包 将pom.xml中多余的内容删除掉&#xff0c;再添加SpringMVC需要的依赖 <?xml version"1.0" encoding"UT…