【Java多线程案例】单例模式

1. 单例模式概念

设计模式:谈到单例模式,我们首先需要知道什么是设计模式,设计模式是软件工程中的一大重要概念,是被广泛认可并使用于解决特定实际问题的代码设计经验,校招中常考的设计模式有单例模式工厂模式 等,而我们需要重点掌握单例模式代码的编写

简单来说,设计模式就是大佬们为了不让我们这些小菜鸟写烂代码而总结出来的代码编写方式

单例模式:单例模式要求类在一个Java进程只能拥有唯一一个实例,而无法创建出多个实例(尝试使用new关键字创建多个实例的时候就会报错)

2. 单例模式代码示例

简单介绍了设计模式以及单例模式的相关概念,光说不练假把式,现在我们就来尝试编写单例模式的代码,在正式编写代码之前,我们需要知道单例模式有两种实现方式:1、饿汉模式;2、懒汉模式,二者之间的差异就在于创建唯一实例的时机不同!

2.1 饿汉模式

2.1.1 代码案例

/*** 单例模式类*/
class Singleton {private static Singleton instance = new Singleton(); // 指向唯一实例public static Singleton getInstance() {return instance;}private Singleton() {};}public class SingletonDemo01 {public static void main(String[] args) {Singleton instance1 = Singleton.getInstance();Singleton instance2 = Singleton.getInstance();System.out.println(instance1 == instance2);}
}

运行结果如图所示:
image.png

2.1.2 代码分析

  1. 首先观察唯一实例的创建时机private static Singleton instance = new Singleton();该实例在类加载的时候就创建完毕,因此称为"饿汉式"
  2. 再来观察单例模式中的构造方法,普通类的构造方法一般是public修饰的,但是单例模式中将构造方法私有化使用private修饰,因此外界尝试使用new关键字创建实例就会报错
  3. 单例模式提供了一个public static修饰的获取唯一实例的方法,外界只能通过调用这个方法来获取该类的唯一实例,这样就实现了该类只能拥有唯一实例的要求

我相信有的小伙伴一定此时忍不住发出疑问!如果使用反射机制那不就可以创建出多个实例了嘛?事实上如此,但是毕竟反射不属于常规方法,毕竟在代码中滥用反射是非常不好的行为!!!

2.2 懒汉模式

2.2.1 代码案例

/*** 懒汉式单例模式*/
class SingletonLazy {private static SingletonLazy instance = null; // 唯一实例public static SingletonLazy getInstance() {if (instance == null) {instance = new SingletonLazy();}return instance;}private SingletonLazy() {};}

2.2.2 代码分析

  1. 懒汉模式的唯一实例并不是在类加载的时候就创建完毕,而是遵循"能不创建就不创建,能晚创建就晚创建"的原则,直到外界有程序调用getInstance方法获取实例时才创建完毕,这就是"懒汉式"的由来
  2. 懒汉模式与饿汉模式一致,都将构造方法设置成私有
  3. 懒汉模式与饿汉模式一致,都提供getInstance方法供外界获取该类的唯一实例

注:在计算机世界中"懒"往往意味着效率高,考虑这样一个场景,有一个10GB的文件,你使用文本编辑器打开,如果采用饿汉式的方式,系统会将10GB的文件一次性加载到内存中,然后统一展示;然而如果使用懒汉式方式打开,则编辑器先加载10KB文件让用户阅读,随着用户进行翻页操作再继续加载10KB文件到内存中。此时懒汉式无疑效率更高

3. 单例模式的线程安全问题

3.1 问题引入

上述关于"单例模式"的介绍只是序幕,毕竟我们本章重点论述的还是多线程主题,下面就有一个重要问题了:上述我们编写的单例模式代码是否存在 线程安全问题 呢?

  • 饿汉模式:由于饿汉模式中创建唯一实例的时机在类加载的时候,而调用getInstance方法执行的过程中直接返回该唯一实例,是纯粹的读取操作,不涉及多个线程修改同一变量的情况,因此天然就是线程安全的!
  • 懒汉模式:想必聪明的小伙伴已经想到了,懒汉模式是不是就是线程不安全的呢?就是这样!下面我们就来考虑多个线程同时执行的情况

演示懒汉模式的线程安全问题
image.png
如图所示:线程t1首先进入条件判断当前实例为null,然后进入if语句块中,但是此时还没有来得及执行new操作创建实例就被调度出CPU,此时线程t2进行判断当前实例为null,然后进入if语句块中创建实例完成返回,此时线程t1重新被CPU调度,接着执行new SingletonLazy()方法,因此创建出了多个实例,即懒汉式单例模式线程不安全!!!

3.2 解决懒汉式线程安全问题

那么如何改进上述代码让"懒汉式"单例模式变成线程安全的呢?出现问题的关键在于if条件判断和new创建实例并不是原子操作,因此解决方法就是通过synchronized关键字将这两个操作打包成原子操作!

3.2.1 改进代码(解决线程安全问题)

/*** 改进版本单例模式(解决线程安全问题)*/
class SingletonLazyImprove01 {private static SingletonLazyImprove01 instance = null; // 唯一实例private static Object locker = new Object();public static SingletonLazyImprove01 getInstance() {synchronized (locker) {if (instance == null) {instance = new SingletonLazyImprove01();}}return instance;}private SingletonLazyImprove01() {};
}

上述代码我们引入synchronized关键字将if条件判断与new创建实例操作加锁打包成"原子"操作,此时如果继续按照刚才的场景,线程t1先进行加锁,如果t2也尝试进入if语句块就会因为锁竞争而阻塞等待,直到线程t1创建完实例之后释放锁,此时t2线程判断实例已经被创建好,就直接返回,因此不会出现线程安全问题了!

3.2.2 改进代码(解决效率问题)

但是问题还没有结束!现在的代码虽然解决了线程安全问题,但是还存在着效率问题,因为我们这里调用getInstance都会先尝试加锁,然后判断当前实例是否为空,然后再解锁!但是线程安全问题只存在于第一次尝试创建实例的时候,如果实例已经创建完毕,后续所有的操作都是读取操作,就不会再产生线程安全问题了!所以我们引入的解决方式就是 if双重校验锁 ,其代码如下:

class SingletonLazyImprove02 {private static SingletonLazyImprove02 instance = null;private static Object locker = new Object();public static SingletonLazyImprove02 getInstance() {if (instance == null) {synchronized (locker) {if (instance == null) {instance = new SingletonLazyImprove02();}}}return instance;}private SingletonLazyImprove02() {};
}

此时我们会看到有两个if判断语句,换做是单线程情况下我们从来不会写这样的代码,但是一旦涉及到多线程的时候,if双重校验锁是很常见的,其中第一个if语句用来判断是否需要进行加锁操作,第二个if语句用来判断是否需要创建实例,但是恰巧两个if语句的条件一样!

3.2.3 改进代码(解决指令重排序问题)

但是上述代码还有一些小问题,我们需要先介绍有关 指令重排序 的话题

指令重排序:指令重排序是一种编译器优化手段,会在保证逻辑不变的情况下调整原有代码的执行顺序,来提高程序的效率,但是有可能会引发线程安全问题
例如在创建实例的代码中:instance = new SingletonLazyImprove02();内部包括三个核心步骤

  1. 申请一块内存空间
  2. 调用构造方法对内存空间进行初始化
  3. 将内存空间地址赋值给instance变量

正常情况下执行顺序都是按照1->2->3进行的,但是编译器也可能会优化为1->3->2的步骤来执行,这两种在单线程环境下都没有问题,但是如果在多线程情况下执行就有可能出现线程安全问题
image.png
如图所示,线程t1先进行加锁,然后创建实例过程中先执行第一步与第三步,此时被调度出CPU,但是后来的线程判断实例是否为空,此时直接返回未被初始化的内存空间地址,这时就会出现错误!!!

volatile关键字:我们在之前的章节已经提到过使用volatile关键字可以解决内存可见性和指令重排序的问题,因此该问题的解决方式就是使用volatile关键字,强制让编译器完全按照1->2->3的顺序来执行

/*** 懒汉式单例模式改进版本(解决指令重排序)*/
class SingletonLazyImprove03 {private static volatile SingletonLazyImprove03 instance = null;private static Object locker = new Object();public static SingletonLazyImprove03 getInstance() {if (instance == null) {synchronized (locker) {if (instance == null) {instance = new SingletonLazyImprove03();}}}return instance;}private SingletonLazyImprove03() {};
}

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

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

相关文章

asp.net core 依赖注入 实例化对象实例

在面向对象编程中,推荐使用面向接口编程,这样我们的代码就依赖于服务接口,而不是依赖于实现类,可以实现代码解耦。 名称解释: 我们把负责提供对象的注册和 获取功能的框架叫作“容器”, 注册到容器中的对象…

CMS 检测神器:CMSeek 保姆级教程(附链接)

一、介绍 CMSeek(Content Management System Exploitation and Enumeration Toolkit)是一款用于检测和利用网站上可能存在的内容管理系统(CMS)漏洞的开源工具。它旨在帮助安全研究人员和渗透测试人员识别目标网站所使用的CMS&…

服务器安装Docker (centOS)

1. 卸载旧版本的Docker(如果有) 首先,如果您的系统上安装了旧版本的Docker,需要将其卸载。Docker的旧版本称为docker或docker-engine。使用以下命令来卸载旧版本: sudo yum remove docker \ docker-client \ docker-…

2024牛客寒假算法基础集训营2部分题解

Tokitsukaze and Bracelet 链接:登录—专业IT笔试面试备考平台_牛客网 来源:牛客网 题目描述 《绯染天空》是一款由 key 社与飞机社共同开发的角色扮演游戏,剧情内容由著名的剧本作家麻枝准编写。它是一款氪金手游,但也有 st…

成为CSDN博客优质创作者或者博客专家吧

成为CSDN博客优质创作者或者博客专家吧 文章目录 成为CSDN博客优质创作者或者博客专家吧一、前言二、如何成为CSDN的博客专家1、2009年的要求和申请方式2、最新的CSDN博客专家要求和申请方式3、创作者身份认证4、CSDN所有认证的介绍 三、写博客的好处1、比较官方的说法&#xf…

Nacos1.X源码解读(待完善)

目录 下载源码 注册服务 客户端注册流程 注册接口API 服务端处理注册请求 设计亮点 服务端流程图 下载源码 1. 克隆git地址到本地 # 下载nacos源码 git clone https://github.com/alibaba/nacos.git 2. 切换分支到1.4.7, maven编译(3.5.1) 3. 找到启动类com.alibaba.na…

波卡 2023 四季度报告:开发者数量位列加密生态前三,五项新技术将于今年发布

作者:Nicholas Garcia|Messari 研究分析师 编译:OneBlock 原文:https://messari.io/report/state-of-polkadot-q4-2023?utm_mediumorganic_social&utm_sourcetwitter_messari&utm_campaignstate_of_polkadot_q4_2023 …

RabbitMQ的延迟队列实现[死信队列](笔记二)

上一篇已经讲述了实现死信队列的rabbitMQ服务配置&#xff0c;可以点击: RabbitMQ的延迟队列实现(笔记一) 目录 搭建一个新的springboot项目模仿订单延迟支付过期操作启动项目进行测试 搭建一个新的springboot项目 1.相关核心依赖如下 <dependency><groupId>org.…

13. UE5 RPG限制Attribute的值的范围以及生成结构体

前面几章&#xff0c;我们实现了通过GameplayEffect对Attribute值的修改&#xff0c;比如血量和蓝量&#xff0c;我们都是有一个最大血量和最大蓝量去限制它的最大值&#xff0c;而且血量和蓝量最小值不会小于零。之前我们是没有实现相关限制的&#xff0c;接下来&#xff0c;我…

小白水平理解面试经典题目LeetCode 71. Simplify Path【Stack类】

71. 简化路径 小白渣翻译 给定一个字符串 path &#xff0c;它是 Unix 风格文件系统中文件或目录的绝对路径&#xff08;以斜杠 ‘/’ 开头&#xff09;&#xff0c;将其转换为简化的规范路径。 在 Unix 风格的文件系统中&#xff0c;句点 ‘.’ 指的是当前目录&#xff0c;…

flutter监听app进入前后台状态的实现

在开发app的过程中&#xff0c;我们经常需要根据app的前后台的状态&#xff0c;做一些事情&#xff0c;那么我们在flutter中是如何实现这一监听的&#xff1f; flutter给我们提供了WidgetsBindingObserver来进行一些状态的判断&#xff0c;但是判断前后台的状态只是该API种其中…

微软.NET6开发的C#特性——接口和属性

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;看到不少初学者在学习编程语言的过程中如此的痛苦&#xff0c;我决定做点什么&#xff0c;下面我就重点讲讲微软.NET6开发人员需要知道的C#特性。 C#经历了多年发展&#xff0c; 进行了多次重大创新&#xf…

为什么要设置止损

2024年1月至2月7日&#xff0c;A股最令人瞩目的事件就是代表小微盘的中证500和中证1000雪球连续敲入&#xff0c;以及万得微盘指数的崩塌&#xff08;1个月下跌50%&#xff09;。 这次的这个过程中&#xff0c;止损很重要。一般情况下&#xff0c;如果设置了20%回撤止损的话&am…

跨品牌智能家居控制_从原理到实现_HomeAssistant

项目地址&#xff1a;https://github.com/home-assistant/core Star&#xff1a;67 K 1 引言 最近去南方玩&#xff0c;住了一些智能酒店&#xff0c;自动开关电视、窗帘、灯、空调&#xff0c;还挺好用的&#xff0c;尤其喜欢关灯这功能。先不说它的理解能力&#xff08;对同…

豪掷770亿!华为员工集体“分红大狂欢”:至少14万人受益

豪掷770亿&#xff01;华为员工集体“分红大狂欢”&#xff1a;至少14万人受益 近日&#xff0c;华为宣布了其2023年度分红计划&#xff0c;总金额高达770.85亿元&#xff0c;预计至少将惠及14万员工。这一消息引发了广泛关注和热议&#xff0c;成为业界的一大亮点。作为中国领…

Go 语言中如何大小端字节序?int 转 byte 是如何进行的?

嗨&#xff0c;大家好&#xff01;我是波罗学。 本文是系列文章 Go 技巧第十五篇&#xff0c;系列文章查看&#xff1a;Go 语言技巧。 我们先看这样一个问题&#xff1a;“Go 语言中&#xff0c;将 byte 转换为 int 时是否涉及字节序&#xff08;endianness&#xff09;&#x…

代码随想录算法训练营第42天 | 01背包理论基础 416.分割等和子集

01背包理论基础 问题定义&#xff1a;有n件物品和一个能装重量为w的背包&#xff0c;第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i]。每件物品只能用一次&#xff0c;求解将哪些物品装入背包获得的总价值最大。dp数组含义&#xff1a;dp[i][j] 表示从下标为 [0…

PHP安装后错误处理

一&#xff1a;问题 安装PHP后提示错误如下 二&#xff1a;解决 1&#xff1a;Warning: Module mysqli already loaded in Unknown on line 0解决 原因&#xff1a;通过php.ini配置文件开启mysqli扩展的时候&#xff0c;开启了多次 解决&#xff1a;将php.ini配置文件中多个…

层层深入揭示C语言指针的底层机制

理解C语言指针的底层机制需要我们从硬件、操作系统和编译器三个层次逐步展开。 1. 硬件层次 计算机硬件是实现内存管理的基础。内存是一个由无数个存储单元组成的线性空间&#xff0c;每个存储单元都有一个唯一的地址。这个地址通常是一个二进制数&#xff0c;表示该存储单元…

Linux网络编程——udp套接字

本章Gitee地址&#xff1a;udp套接字 文章目录 创建套接字绑定端口号读取数据发送数据聊天框输入框 创建套接字 #include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);int domain参数&#xff1a;表面要创建套接字的域…