【Java系列】多线程案例学习——单例模式

news2024/10/5 18:53:34

个人主页:兜里有颗棉花糖
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创
收录于专栏【Java系列专栏】【JaveEE学习专栏】
本专栏旨在分享学习JavaEE的一点学习心得,欢迎大家在评论区交流讨论💌
在这里插入图片描述

目录

  • 一、设计模式
  • 二、单例模式
    • 饿汉模式
    • 懒汉模式
  • 三、线程安全问题
    • 饿汉模式的线程安全问题
    • 懒汉模式的线程安全问题
    • 指令重排序

一、设计模式

在讲解单例模式之前,我们先来看一下什么是设计模式。

在实际的软件开发中,我们肯定会碰到很多典型的实际问题来进行解决,而针对这些实际的问题有的人就总结出了特定的一套解决方案来进行问题的解决。

设计模式中就提供给了我们很多典型场景的解决问题的处理方式。

二、单例模式

什么是单例模式:
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例(比如JDBC中的DataSource实例就只需要一个,即使我们实例出了多个DateSource对象的话我们此时描述的依然是同一个服务器,这是完全没有必要的)。

在单例模式中又分为两种模式,一种是饿汉模式,一种是懒汉模式。

  • 饿汉模式:程序启动,类加载之后就会立即创建出实例。
  • 懒汉模式:在第一次使用实例的时候在创建实例,否则能不创建实例就不创建实例。

饿汉模式

我们来写一段代码进行单例模式(饿汉模式)的举例,请看:

class Singleton {
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
}

public class Demo17 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

代码运行结果如下:
在这里插入图片描述
解释:上述代码使用饿汉式单例模式来实现单例的创建,即在类加载的时候就已经创建好了实例,并且在整个应用程序的生命周期中都只保留了一个实例。
所以最后打印出来的结果是true,因为引用s1和引用s2指向的对象是同一个对象。
再来看下图代码的运行结果:
在这里插入图片描述
引用s1和引用s3指向的对象并不是同一个对象。

现在我们需要对上述代码进行限制,来禁止创建Singleton类的实例(只需要把类Singleton的构造方法的权限设置为private),更改之后的代码如下:
在这里插入图片描述
此时如果我们再向创建Singleton的实例的话就会报错,报错信息如下:
在这里插入图片描述
此时我们能够使用的Singleton实例就有且只有一个了。如果想要获得Singleton的实例的话只能通过get方法来进行获取,并且获取到的对象一定是一个唯一的对象。

懒汉模式

我们依然是写一段代码来进行举例,请看:

class Singletonlazy {
    private static Singletonlazy instance = null;
    public static Singletonlazy getInstance() {
        if(instance == null) {
            instance = new Singletonlazy();
        }
        return instance;
    }
    private Singletonlazy() {}
}

public class Demo18 {
    public static void main(String[] args) {
        Singletonlazy s1 = Singletonlazy.getInstance();
        Singletonlazy s2 = Singletonlazy.getInstance();
        System.out.println(s1 == s2);
    }
}

运行结果如下:
在这里插入图片描述
上述代码依旧是无法创建多个实例的,报错信息如下:
在这里插入图片描述
上述写法就是单例模式中的懒汉模式的。

三、线程安全问题

我们现在来看看饿汉模式和懒汉模式中的线程安全问题。

饿汉模式的线程安全问题

在这里插入图片描述

饿汉模式中的线程安全问题解释:当我们多次调用getInstance方法的时候,并不会修改实例instance的内容,同时我们直到多线程读取同一个变量的时候,此时是不会出现线程安全的问题的,因为多线程读取同一个变量的时候是不会对变量进行修改的;因为在这里并不会修改instance实例中的内容。综上,饿汉模式并不会引起线程安全的问题。

懒汉模式的线程安全问题

在这里插入图片描述
线程A获取到锁,并创建了一个新的Singletonlazy实例并将其赋值给instance。
然后,线程B获取到锁,由于此时instance不为null,线程B也会创建一个新的Singletonlazy实例并将其赋值给instance。这样,就导致了多个实例的创建,违反了单例模式的定义。

在这里插入图片描述
上图就很好的演示了为什么懒汉模式在多线程下会创建出多个实例,即违背了单例模式的初衷。

所以懒汉模式中这里的线程是不安全的。我们可以通过加锁操作来解决上述问题:
请看下图代码,我们能不能通过这种方式来进行加锁呢,请看:
在这里插入图片描述
上述的加锁操作的写法是错误的,并没有解决线程安全问题。
正确的写法是这样的,请看下图:
在这里插入图片描述
在这里插入图片描述
同时我们要知道加锁的基本原则应该是非必要不加锁(加锁本身是一个成本比较高的操作,加锁之后就有可能引起其它线程的等待阻塞)。
在上述正确的加锁之后的代码中每次调用getInstance方法都要进行加锁操作,但是这样的加锁其实是没有必要的(懒汉模式的线程不安全的问题最主要的是出现在实例刚刚创建的时候,一旦实例创建好了之后我们其实就没必要进行加锁操作了,后续再调用getInstance方法的时候也就不存在线程不安全的问题了)。

在这里插入图片描述
请看上图,当实例创建好了之后,getInstance方法中的if判断就永远不会成立,所以上图代码的加锁方式其实是不合适的(因为我们每次调用getInstance方法都需要进行加锁操作),所以我们需要再次对代码进行修改(需要再添加一个if判断)。
修改之后的代码如下图:
在这里插入图片描述
如上图,相同的if判断我们写了两遍,但是这两个if判断之间的执行间隔时间可能是非常长的(加锁操作所引起的阻塞等待时间是不确定的,也有可能时间是非常长的),在这段间隔时间内,其它线程很有可能对instance实例进行修改,所以我们再添加一个if判断是非常有必要的(即一共有两个相同的if判断)。
其实,这两个if判断条件之所以一样也算是一个巧合,第一个if判断是为了判断是否要进行加锁操作,第二个if判断是为了判断是否要创建实例

然而修改后的上述代码依然存在一些其它的问题,比如是否能够保证内存可见性,比如:在一个多线程环境下,线程A和线程B同时调用了getInstance方法,线程A获得了锁,并在检查instance变量为null后,实例化出来一个instance对象,然后将其赋值给instance变量。然而,在这个赋值操作完成之后,由于内存可见性可能无法保证(因为我们不知道编译器是否会对我们的代码进行优化),此时线程B中第二个if判断读取到的instance的值依然是null值,所以第二个线程,即线程B可能也会创建出来一个instance对象。所以此时我们为例能够保证内存可见性,我们此时需要使用volatile对instance变量进行修饰,以保证内存可见性。

另外,上述解释中我们使用了volatile对instance变量进行修饰其实还有另外一个用途,就是防止指令重排序(指令重排序也是线程不安全问题的一个重要原因)。

指令重排序

指令重排序也是编译器对我们代码进行优化的一种手段,即保证代码在原有逻辑不变的情况下,对代码的执行顺序进行一些调整,从而是调整之后的代码的执行效率有所提高。

在这里插入图片描述
如上图,创建出实例对象这个操作站在指令的角度就可以分为三个步骤进行执行:

  • 第一步:给对象创建出内存空间,得到内存地址
  • 第二步:在空间上调用内存方法,对对象进行初始化
  • 第三步:把内存地址赋值给instance引用

上述指令的三个操作由于编译器优化,三个指令的执行顺序就有可能发生改变。
执行完第一步之后对于当单线程而言,先执行第二步还是先执行第三步其实都是可以的。
但是如果是多线程环境下,二、三、步骤的执行顺序进行颠倒的话就有可能出现问题。

举个栗子,假设上述指令是按照132的顺序进行执行的,即还没来得及对对象进行初始化就调度给其它线程了,当第二个线程执行先判定instance不为空然后返回instance的时候,并且可能会使用instance实例对象中的一些属性和方法,但是我们得到的instance对象是一个不完整的对象(即没有进行初始化的对象)。由于这里我们得到的是一个不完整的对象(即该对象并没有被完全的初始化),所以后续我们使用这个对象的时候很大可能就会出现一些问题(因为我们使用的这个对象是一个不完整的对象啊)。
当然,上述出现的问题比较极端,为什么极端我们再来分析一下加深印象:第一点:new对象的指令是按照132的顺序执行的;第二点:在执行3指令之后2指令之前恰好出现了线程调度;第三点:恰好线程调度切换的时候,切换到的那个线程返回了一个未被初始化的一个instance对象。
所以,我们使用volatile对instance变量进行修饰之后就不会出现上述的指令重排序问题,即new对象指令一定会按照123的顺序进行执行。

最终代码版本如下图,请看:
在这里插入图片描述

好了,以上就是关于上述单例模式代码的书写,一共有三个点需要我们注意:加锁操作、双重if的判断、volatile关键字。

本文到这里就结束了,希望友友们可以支持一下一键三连哈。嗯,就到这里吧,再见啦!!!

在这里插入图片描述

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

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

相关文章

第七节TypeScript 循环

1、简述 有的时候,我们可能需要多次执行同一块代码。一般情况下,语句是按顺序执行的:函数中的第一个语句先执行,接着是第二个语句,依此类推。 循环语句允许我们多次执行一个语句或语句组。 循环语句流程图&#xff…

Mybatis-Plus——02配置SQL日志,CRUD增(新注解)

配置SQL日志,CRUD增 一、配置SQL日志二、CRUD——增insert2.1、主建生成策略2.2、主建设置自增插入————————如觉不错,随手点赞,关注,收藏(* ̄︶ ̄),谢谢~~ 一、配置SQL日志 mybatis-plus…

网络编程 DAY6 作业

1.使用数据库创建一个工人信息库,包含工号(主键)、姓名、年龄、薪资 2.添加三条工人信息(可以完整信息,也可以非完整信息) 3.修改某一个人的薪资(确定的一个) 4.展示出工资在10000到…

Linux目录和文件管理

一.Linux目录结构 Linux操作系统在定位文件或目录位置时,使用斜杠“ / ”进行分割(区别于Windows操作系统中的反斜杠“ \ ”)。整个树形目录结构中,使用独立的一个" / "表示根目录,根目录是Linux操作系统文…

传输层协议分析--第4关:UDP 包分析

任务描述 本关任务:能够掌握简单的 UDP 包分析。 相关知识 为了更好掌握本章内容,你需要了解的有: UDP 报文的简介;UDP 报文格式;Wireshark 软件中的 UDP 抓包分析。 UDP 简介 UDP(User Datagram Pro…

【终极教程】cocos2dx-js 分批次混淆压缩js文件

说明: 1> 由于我们当前游戏框架的结构是平台形式的就是一个大厅里面有若干个子游戏,所以在发布的时候得区分子游戏和大厅了解了一下 project.json 里面有一个 jsList 可以把所有放进去的js文件压缩混淆成一个大的文件但是我们游戏的子游戏非常多 这样子弄显然不合适&#x…

如何设计树形结构

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO 联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬 前置知识:前…

代码随想录算法训练营Day6 | 344.反转字符串、541.反转字符串||、替换数字、151.反转字符串中的单词、右旋字符串

LeetCode 344 反转字符串 本题思路:反转字符串比较简单,定义两个指针,一个 i 0, 一个 j s.length-1。然后定义一个临时变量 tmp,进行交换 s[i] 和 s[j]。 class Solution {public void reverseString(char[] s) {int i 0;int …

工程项目管理系统源码:Java版工程项目管理系统平台助力工程企业迈向数字化管理的巅峰

随着企业规模的不断扩大和业务的快速发展,传统的工程项目管理方式已经无法满足现代企业的需求。为了提高工程管理效率、减轻劳动强度、提高信息处理速度和准确性,企业需要借助先进的数字化技术进行转型。本文将介绍一款采用Spring CloudSpring BootMybat…

创新铸就卓越 HashData再度荣获 IT168技术卓越奖

12月20日,由国内专业数据库及架构技术社区 ITPUB、 IT 垂直门户IT168 联合主办的“2023 技术卓越奖”评选结果正式揭晓, 酷克数据基于云原生架构打造的HashData云数仓,凭借创新的技术架构、强大的性能表现、行业领先的落地规模,连…

在GitHub找开源项目

在 GitHub 的搜索框里: 使用搜索关键词可以在 GitHub 上快速的找你需要的开源项目: 限制搜索范围 通过 in 关键词 (大小写不敏感) 限制搜索范围: 公式搜索范围in:name xxx项目名包含xxxin:description xxx项目描述包含xxxin:readme xxx项目…

记录 | gdb调试的基本命令

r (run) 运行程序 b (breakpoint) 打断点,比如 b func(打到函数) b 5(打到第5行)(当前文件) b main.cpp:5(main.cpp的第5行) b MyClass::func() (打到类的成员函数func()、在类内可以 p this 打印、p *this、p this->name) tb (temporary breakpoint) 临时断…

算法基础之欧拉函数

欧拉函数 核心思想&#xff1a;欧拉函数: 证明 &#xff1a;容斥原理 #include<iostream>#include<algorithm>using namespace std;const int N 110;int main(){int n;cin>>n;while(n--){int a;cin>>a;int res a;for(int i 2;i < a/i; i){if(a %…

使用TikTok云手机轻松拓展全球市场

TikTok作为一款风靡全球的短视频应用&#xff0c;全球影响力不断扩大。越来越多的商家开始借助TikTok分享作品、在海外市场上获取商业机会。要想更好地借助TikTok扩大海外市场&#xff0c;使用TikTok云手机是一个好选择。本文将介绍TikTok云手机的几大作用&#xff0c;以助您更…

解决ESP8266无法退出透传问题以及获取网络时间以及天气方法

网上很多配置ESP8266的教程&#xff0c;但是遇到无法退出透传模式的情况却没有找得到答案&#xff0c;不知道是大家都没遇到还是怎么样&#xff0c;以下是我的解决方法&#xff1a;实测有效 先发送“”&#xff08;三个加号&#xff09;&#xff08;如果是在串口调试助手调试&…

保存Google Colab数据文件或目录到Google Drive云盘

一、背景 在Google Colab上使用T4 GPU完成微调训练的模型&#xff0c;保存了模型和训练状态的文件存放在Google Colab /content目录内&#xff0c;需要备份。考虑到下载到本地需要大量的流量&#xff0c;且下次继续训练还会上传&#xff0c;最后确定直接保存到Google Drive&am…

linux 上安装 minio

第一步&#xff0c;下载 wget https://dl.minio.org.cn/server/minio/release/linux-amd64/minio 第二步&#xff0c;修改权限 chmod x minio 第三步&#xff0c;设置 Path mv minio /usr/local/bin/ 第四步&#xff0c;创建 minio mkdir minio 第五步&#xff0c;启动 …

C#中var、object和dynamic的区别

在C#编程语言中&#xff0c;我们经常会遇到var、object和dynamic这三个关键字。它们都用于声明变量&#xff0c;但在使用方法和特性上存在一些重要的区别。本文将详细介绍这三者的差异。 目录 var关键字object关键字dynamic关键字总结 var关键字 var是C#语言中的隐式类型推断…

JDBC学习,从入门到入土

JDBC引入 JDBC概念&#xff1a; JDBC是使用Java语言操作关系型数据库的一套API。全称&#xff1a;&#xff08;Java DataBase Connectivity&#xff09;Java数据库连接 JDBC的本质&#xff1a; 官方定义的一套操作所有关系型数据库的规则&#xff0c;即接口。 各个数据库厂…

Windows操作系统重装【老毛桃、软碟通、硬盘装机以及装机原理介绍】

目录 一、老毛桃装机 1.1 老毛桃介绍 1.2 注意事项 1.3 操作步骤 1.3.1 老毛桃U盘制作 1.3.2 老毛桃u盘启动 1.3.3 老毛桃u盘重装系统 二、软碟通装机 2.1 软碟通介绍 2.2 主要特点及功能 2.3 操作步骤 2.3.1 用软碟通制作U盘安装盘 2.3.2 U盘启动 2.3.3 安装系统…