当前位置: 首页 > news >正文

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

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


文章目录

  • 一. 什么是单例模式
  • 二. 实现单例模式
    • 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. 对内存要求不高的场景中可以直接使用饿汉式写法;对内存要求高的场景下,可以使用懒汉式写法,在需要使用时才创建对象。

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

相关文章:

  • 决策树(Python)
  • [第二章—Spring MVC的高级技术] 2.3 处理异常
  • 算法训练营第五十七天|647. 回文子串 516.最长回文子序列
  • 252.【2023年华为OD机试真题(C卷)】局域网中的服务器个数(优先搜索(DFS)-JavaPythonC++JS实现)
  • [已解决]504 Gateway Time-out 网关超时
  • 机器学习第一个项目-----鸢尾花及报错解决
  • unity学习笔记----游戏练习06
  • ZigBee学习——浅析协议栈
  • 网络协议与攻击模拟_08DHCP协议
  • Object.prototype.toString.call个人理解
  • Docker数据管理
  • BTC交易模式 - UXTO - 工具整理
  • 鸿蒙开发案例002
  • 数据结构笔记1
  • php 文件上传
  • 11111111
  • ERLANG 网工修炼笔记 ---- UDP
  • Iterator 和 for...of 循环
  • JavaScript设计模式系列一:工厂模式
  • Service Worker
  • Spring Boot MyBatis配置多种数据库
  • sublime配置文件
  • Transformer-XL: Unleashing the Potential of Attention Models
  • V4L2视频输入框架概述
  • webpack4 一点通
  • 程序员最讨厌的9句话,你可有补充?
  • 后端_MYSQL
  • 精益 React 学习指南 (Lean React)- 1.5 React 与 DOM
  • 容器服务kubernetes弹性伸缩高级用法
  • 通过npm或yarn自动生成vue组件
  • 小程序测试方案初探
  • 学习使用ExpressJS 4.0中的新Router
  • 移动端 h5开发相关内容总结(三)
  • 做一名精致的JavaScripter 01:JavaScript简介
  • 扩展资源服务器解决oauth2 性能瓶颈
  • ​一帧图像的Android之旅 :应用的首个绘制请求
  • $ is not function   和JQUERY 命名 冲突的解说 Jquer问题 (
  • $var=htmlencode(“‘);alert(‘2“); 的个人理解
  • (2021|NIPS,扩散,无条件分数估计,条件分数估计)无分类器引导扩散
  • (PWM呼吸灯)合泰开发板HT66F2390-----点灯大师
  • (附源码)springboot教学评价 毕业设计 641310
  • (附源码)ssm考试题库管理系统 毕业设计 069043
  • .NET Core 2.1路线图
  • .net访问oracle数据库性能问题
  • .net开源工作流引擎ccflow表单数据返回值Pop分组模式和表格模式对比
  • .NET下的多线程编程—1-线程机制概述
  • @RequestParam详解
  • [20140403]查询是否产生日志
  • [AIR] NativeExtension在IOS下的开发实例 --- IOS项目的创建 (一)
  • [Android Studio] 开发Java 程序
  • [ASP.NET 控件实作 Day7] 设定工具箱的控件图标
  • [BJDCTF2020]The mystery of ip1
  • [BZOJ2281][SDOI2011]黑白棋(K-Nim博弈)
  • [C#]猫叫人醒老鼠跑 C#的委托及事件
  • [codeforces] 25E Test || hash