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

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


文章目录

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯官网填空题(01串的熵)

问题描述 答案提交 这是一道结果填空的题, 你只需要算出结果后提交即可。本题的结果为一 个整数, 在提交答案时只填写这个整数, 填写多余的内容将无法得分。 import java.util.*;public class Main {public static void main(String[] args) {for(double zero1;zero<2333…

开始学习vue2(Vue方法)

一、过滤器 过滤器&#xff08;Filters&#xff09;是 vue 为开发者提供的功能&#xff0c;常用于文本的格式 化。过滤器可以用在两个地方&#xff1a;插值表达式 和 v-bind 属性绑定。 过滤器应该被添加在 JavaScript 表达式的尾部&#xff0c;由“管道符 ”进行 调用&#…

【Linux】常见指令(二)

前言 常见指令第二部分。 文章目录 一、指令&#xff08;下&#xff09;重定向>&#xff1a;输出重定向>>&#xff1a;追加输出<&#xff1a;输入重定向 10. more—显示文本文件内容11.less—逐屏浏览文本文件内容12. head13. tail管道 |14. date—时间指令在这里插…

5个程序员可以接私活的平台和一些建议

22年之前我从没有接触过程序员外包接单&#xff0c;也没有任何的私活接单经验&#xff0c;就纯纯看自己瞎摸索&#xff0c;通过Google搜索&#xff0c;在各类程序员私活接单平台上摸爬滚打&#xff0c;硬是杀出一条血路&#xff0c;从一开始的年入3k到现在每月稳定收入1w&#…

STL第四讲

第四讲 万用Hash Function 左侧的是设计为类并重载调用运算符&#xff0c;右侧是一般函数的形势&#xff1b; 但是右侧形势在创建容器时更麻烦&#xff1b; 具体例子&#xff1a; 第三种形势&#xff1a;struct hash 偏特化形式 tuple 自C03引入&#xff1b; 关于源码解读的…

Xcode 15 libarclite 缺失问题

升级到Xcode 15运行项目报错&#xff0c;报错信息如下&#xff1a; SDK does not contain libarclite at the path /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a; try increasing the minimum d…

2024年学鸿蒙开发有前途吗?

随着科技的不断发展和智能设备的普及&#xff0c;鸿蒙系统作为华为自主研发的操作系统&#xff0c;正逐渐受到市场的关注。2024年&#xff0c;学鸿蒙开发是否有前途&#xff0c;成为了很多开发者和学生关心的问题。本文将从多个角度分析鸿蒙系统的发展前景&#xff0c;以及学习…

elment-plus如何引入scss文件实现自定义主题色

elment-plus如何引入scss文件实现自定义主题色&#xff01;如果您想修改elementPlus的默认主题色调&#xff0c;使用自定义的色调&#xff0c;可以考虑使用官方提供的解决办法。 第一步你需要在项目内安装sass插件包。 npm i sass -D 如图&#xff0c;安装完成后&#xff0c;你…

Redisson 分布式锁解决主从一致性问题的原理

目录 一、主从不一致产生原因 二、Redisson 解决主从一致性的原理 一、主从不一致产生原因 1. Redis 主从集群&#xff1a;主从读写分离&#xff0c;主节点将数据同步给从节点 主节点&#xff1a;增删改从节点&#xff1a;读 2. 主从同步存在延迟&#xff0c;若主节点宕机…

Borze(与连续元素有关的题目)

总结&#xff1a;碰到与连续元素有关的题目&#xff0c;可以考虑在某种条件下加i&#xff1b;之类的。

valgrind使用

文章目录 简介安装如何使用valgrind来检测内存错误&#xff1f;如何使用其它的工具&#xff1f;总结 简介 Valgrind是一个工具集&#xff0c;包含了许多调试与性能分析的工具。其中使用最多的是Memcheck&#xff0c;它能帮你检测C/C中的内存问题&#xff0c;避免程序崩溃或不可…

JS之打地鼠案例

需要素材的同学可以私信我 效果图&#xff1a; 上代码&#xff1a; <!DOCTYPE html> <html> <head><meta charset"utf-8"><title></title><style>* {margin: 0;padding: 0;}.box {position: relative;width: 320px;heigh…

【Java程序员面试专栏 专业技能篇】MySQL核心面试指引(三):性能优化策略

关于MySQL部分的核心知识进行一网打尽,包括三部分:基础知识考察、核心机制策略、性能优化策略,通过一篇文章串联面试重点,并且帮助加强日常基础知识的理解,全局思维导图如下所示 本篇Blog为第三部分:性能优化策略,子节点表示追问或同级提问 读写分离 分布式数据库的…

【学网攻】 第(4)节 -- 交换机划分Vlan

文章目录 【学网攻】 第(1)节 -- 认识网络 【学网攻】 第(2)节 -- 交换机认识及使用【学网攻】 第(3)节 -- 交换机配置聚合端口 前言 网络已经成为了我们生活中不可或缺的一部分&#xff0c;它连接了世界各地的人们&#xff0c;让信息和资源得以自由流动。随着互联网的发展&am…

大模型微调实战笔记

大模型三要素 1.算法&#xff1a;模型结构&#xff0c;训练方法 2.数据&#xff1a;数据和模型效果之间的关系&#xff0c;token分词方法 3.算力&#xff1a;英伟达GPU&#xff0c;模型量化 基于大模型对话的系统架构 基于Lora的模型训练最好用&#xff0c;成本低好上手 提…

vue3和vite项目在scss中因为本地图片,不用加~

看了很多文章说要加~&#xff0c;真的好坑哦&#xff0c;我的加了~反而出不来了&#xff1a; 304 Not Modified 所以需要去掉~&#xff1a; /* 默认dark主题 */ :root[themered] {--bg-color: #0d1117;--text-color: #f0f6fc;--backImg: url(/assets/images/redBg.png); }/* …

代码随想录算法训练营第四十二天|01背包问题、01背包问题(滚动数组)、416. 分割等和子集

题目&#xff1a;01背包问题 文章链接&#xff1a;代码随想录 视频链接&#xff1a;LeetCode:背包问题 题目链接&#xff1a;卡码题目链接 图释&#xff1a; //二维dp数组实现 #include <bits/stdc.h> using namespace std;int n, bagweight;// bagweight代表行李箱空…

【Java并发】聊聊Future如何提升商品查询速度

java中可以通过new thread、实现runnable来进行实现线程。但是唯一的缺点是没有返回值、以及抛出异常&#xff0c;而callable就可以解决这个问题。通过配合使用futuretask来进行使用。 并且Future提供了对任务的操作&#xff0c;取消&#xff0c;查询是否完成&#xff0c;获取结…

SpringBoot整合ElasticSearch实现分页查询

本文使用SpringBoot整合ElasticSearch实现分页查询 文章目录 环境准备分页查询方式一方式二 本文小结 环境准备 还是继续使用spring-boot-starter-data-elasticsearch来实现分页查询操作 <!-- spring-boot-starter-data-elasticsearch--> <dependency><groupId&…

QT解析json数据

QT解析json数据 头文件jsonObjectToMapparseJson结果 头文件 #include <QFile> #include <QJsonDocument> #include <QJsonObject> #include <QJsonArray> #include <QJsonValue>jsonObjectToMap 将Json对象转换成map QVariantMap MainWindow…