【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?

文章目录

1 单例模式回顾

单例模式是设计模式的一种。而设计模式就是针对我们实际开发中写代码所遇到的不同场景所设立的解决方案。在笔者JavaSE阶段的文章中,我们曾经聊到过单例模式,这里给出文章链接,有兴趣的读者可以自行阅读:单例模式懒汉式与饿汉式的实现 ,在本文中,笔者也会进行必要的总结。下面,进入正文:

【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?

所谓类的单例设计模式,就是采用一定的方法保证在整个软件系统中,对某一个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。常见的实现方式有两种,懒汉式和饿汉式。

这里的懒汉和饿汉可以这样理解, 对于饿汉来说,肚子很饿,是比较着急去吃饭的,体现出一个着急的态度;而对于懒汉来说,在计算机中,这属于褒义词,指的是”不着急”,只有当你需要的时候,我才给你提供需要的东西,反而更加高效。 所以说:

  1. 饿汉式的单例模式,是比较着急去创建对象实例的;
  2. 懒汉式的单例模式,不太着急去创建对象实例,只有在需要的时候,才去创建。

现在,饿汉式和懒汉式的区别我们了解清楚了, 无非就是创建对象的时机不同。 但是,无论是哪种实现方式,都需要满足一个大前提: 对于该类,只能存在一个对象实例。

因此,为了做到这点,我们可以考虑将构造器私有化,这样在外部就没有办法手动创建对象。同时,在类中,我们需要手动创建一个对象,并对外提供一个公有的方法,使得该创建的对象,能够被外部所获取。

下面我们来具体聊一聊,饿汉式与懒汉式该如何实现单例模式。

【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?

; 2 饿汉式单例模式的实现

对于饿汉式的实现,主要步骤如下:(饿汉主要体现在直接创建对象)

  1. 先将构造器私有化, 避免用户直接在外部 new 新对象。
  2. 在类的内部属性部分直接创建一个对象(我们已经将构造器私有化了,因此在外部没有办法 new 新对象,所以只能在类的内部创建对象)。
  3. 向外提供一个公共的静态方法,用于外部引用该对象。(创建好了对象后,如果对外不提供公共方法,我们就没有办法使用这个被创建的对象。)

这里有一点值得说明, 静态?为什么需要使用static去修饰对象和方法呢?

在类的外部,我们无法使用 new 新建对象,也就是说,我们只能通过 类名.xxx 的方式来返回创建的对象,因此,获取对象的对外公共方法需要使用 static 关键字修饰。而该方法中,需要返回类中创建的对象,静态方法只能使用静态变量,因此,对象属性也应当设置为 static。

实现代码如下:


@SuppressWarnings({"all"})
public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}

3 懒汉式单例模式的实现

对于懒汉式的实现,主要步骤如下:(懒汉主要体现在不直接创建对象,而是在需要的时候再创建)

  1. 先将构造器私有化, 避免用户直接在外部 new 新对象。
  2. 在类的内部 声明一个对象引用,但是不创建, 即先不 new;
  3. 向外提供一个公共的静态方法,用于创建对象并返回对象引用。 需要在该方法中创建对象,并返回给外部。

实现代码如下:


@SuppressWarnings({"all"})
public class Singleton2 {

    private static Singleton2 instance = null;

    private Singleton2(){}

    public static Singleton2 getInstance(){
        if (instance == null){
            instance = new Singleton2();
        }
        return instance;
    }
}

4 单例模式的线程安全问题分析

以上,我们成功实现了饿汉式与懒汉式的单例模式。在串行、单线程环境中,饿汉式和懒汉式实现的单例模式都没有什么问题。那么,如果是在多线程环境呢?这样就存在了一些线程安全问题。

什么是线程安全问题?

线程安不安全,指的是在多线程环境中,代码是否会产生bug。如果,在多线程环境中,不会产生bug,那么我们就说,这是线程安全的;如果,在多线程环境中,会产生bug,我们就说这样的实现方式是不安全的。

什么样的情况下会导致线程安全问题呢?

如果多个线程,只是读取同一个变量、资源,而不作修改,此时仍然是线程安全的。如果,存在修改,则可能会出现线程安全问题。

对于饿汉式单例模式的实现,由于其 仅仅只是读取了变量的内容,因此, 是线程安全的!

【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?

而懒汉式单例模式的实现,就不一样了。在懒汉模式中, 既包含了读,又包含了写,且读写还不是原子的(分成了两个步骤~), 所以 存在线程安全问题。

比如:两个进程g1,g2同时进入该方法,需要返回给 g1 ,g2 对象实例。当 g1 进入该方法后,进行判断,不为 null,因此创建一个对象,而在创建对象的过程中,g2 刚好也进入了该方法,也被判断成了不为 null,此时,g1, g2就指向不同的对象了,所以它是线程不安全的。

【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?
可能文字叙述,还是有些抽象,下面,对例子进行画图描述,以时间为顺序,同样,进程还是用 g1,g2来表示:
【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?
由于进程调度的时机是不确定的,以上只是列举了一种可能导致线程安全问题的调度方式。

那么如何保证懒汉式单例模式的实现是线程安全的呢?

【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?

; 5 线程安全的懒汉式实现

如何实现线程安全?
很简单!既然你说读和写分成了两个步骤,会出现线程安全问题,那我 加锁 不就完了!

【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?
嗯,很好,这样就保证了,在进行读和写操作的时候,只有一个线程在操作,就可以避免另一个线程同时进入 if 语句去创建对象了!

可是,这样真的好吗?

我们简单分析一下,对于懒汉式的实现,是何时会导致线程不安全?

对于懒汉式的代码来说,线程不安全是发生在 instance 被初始化之前!!!在未初始化的时候,即 instance == null,多个线程去调用 getInstance 可能就同时涉及到读和改的问题,引发线程安全问题。
但是,一旦 instance 不是null,已经被初始化后,getInstance方法就只剩下读操作了,此时一定是线程安全的。

那么,我们刚刚的加锁方式,就有了些问题: 无论是 instance 初始化之前,还是 instance 初始化之后,都会导致阻塞,存在大量的锁竞争!

而我们知道,对于加锁操作来说, 加锁确实可以让代码保证线程安全,但是也会付出相应的代价, 最典型的代价就是程序运行变慢了。本来我多个线程运行的好好的,你给我加了把锁,其他线程不就进不去了吗?(一定程度上的阻塞)

因此,我们需要做出如下改进:再加一个判断!判断 instance 是否被初始化,如果已经初始化,就不必进入加锁的代码块了!

【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?
但是!这样还会有问题,即 内存可见性 问题。

如果多个线程,都去调用 getInstance方法,就会造成大量的读 instance 操作。而,我们知道,对于在内存读和在寄存器读来说,后者的效率更高。因此,编译器会自作主张,对代码进行优化: 将读内存操作优化成读寄存器操作。
也就是说,即使某个线程将instance初始化了,但是由于编译器的优化,在其他线程的视角看起来,instance 还是null。

也就是说,这里的内存可见性问题,会导致第一个if语句失效,即不该加锁的时候还会加锁。因此,我们需要保证内存可见!即使用 volatile 对 instance 进行修饰。同时,使用 volatile 可以防止 指令重排序

【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?

这里就有必要对 volatile 关键字的作用进行进一步的解释了:
对于某个共享变量,每个操作单元都缓存一个该变量的副本。当一个操作单元更新其副本时,其他的操作单元可能没有及时发现,进而产生缓存一致性问题。 在Java中也有这样的案例。假设有一个变量a=1,它被3个线程所共享。Jvm在运行时,每个线程都会在自己的工作空间(高速缓存保存的副本,不是栈区)保存一份变量a的副本。如下图。

【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?
【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?
为了解决类似这样的问题,Java则有了些”规定”,对于多个线程同时操作 volatile变量 ,那么 对该变量的写操作必须在读操作之前执行(禁止重排序), 并且写操作的结果对读操作可见(强缓存一致性)。在之前的例子中,如果把变量a用volatile声明,那么当线程t1更新a=0时,与之并发执行的线程t2和线程t3读到的a的值就会是0了。

那么什么是指令重排序呢?

我们以下面这段代码举例

instance = new Singleton();

分为三个步骤:

  1. 创建内存
  2. 针对内存进行初始化
  3. 把内存地址赋值给引用

而在多线程环境中,这三个步骤可能会触发指令的重排序,即可能执行顺序不是123而是132。此时在其他线程中,就有可能看到一个非null的引用,但是实际访问的时候会出现一个非法的对象。加了 volatile 就可以 保证指令是按照1、2、3的顺序执行,保证其他线程拿到的实例也是一个正确完整的实例。

完整代码如下:


@SuppressWarnings({"all"})
public class Singleton2 {

    private static volatile Singleton2 instance = null;

    private Singleton2(){}

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

6 总结

对于饿汉式单例模式,由于只存在对同一变量的读操作,不会导致线程安全问题。

对于懒汉式单例模式,实现线程安全主要有如下三步:

  1. 在正确的位置加锁!
  2. 双重 if 判定!
  3. volatile!

参考文章:对volatile的理解

以上便是本文的全部内容啦!创作不易,如果你有任何问题,欢迎私信,感谢您的支持!

【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?

Original: https://blog.csdn.net/m0_60353039/article/details/128697900
Author: 兴趣使然黄小黄
Title: 【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/814287/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 利用pandas拆分单元格并进行分组聚合

    背景: 最近指导老师布置了一个数据分析的任务要将三张表格进行整理 得到错题的数目 页行列表示错题的位置 思路: 我的思路是读取文件夹当中的文件名列表,然后利用for循环和panda…

    Python 2023年8月7日
    053
  • ChatGPT写的一篇数字化转型文章

    文末附ChatGPT调戏链接,无需注册登录直接使用。 数字化转型是指企业利用数字技术来改善业务流程、增强竞争力和降低成本。它旨在帮助企业更快、更有效地实现业务发展和创新,并为客户提…

    Python 2023年11月4日
    039
  • ubuntu18.04解决问题:is not a supported wheel on this platform(已解决)–用python3.8安装crala0.9.13时出现

    ubuntu18.04解决问题:is not a supported wheel on this platform(已解决)–用python3.8安装crala0.9.13时出现 …

    Python 2023年9月20日
    065
  • Python基础入门:正则re.sub使用自定义替换方法

    在使用正则替换时,有时候需要将匹配的结果做对应处理,便可以使用自定义替换方法。re.sub的用法为: re.sub(‘匹配表达式’,替换字符或替换函数,替换次数,其他参数) 例如,…

    Python 2023年11月9日
    025
  • 机器学习之基础知识(全)

    目录 1.机器学习概述 1.1 人工智能概述 1.1.1 人工智能使用场景 1.1.2 人工智能小案例 1.2 人工智能发展历程 1.2.1 图灵测试 1.2.2 发展历程 1.2…

    Python 2023年8月23日
    046
  • JAVA学习笔记- – – day 1

    💕前言:作者是一名正在学习JAVA的初学者,每天分享自己的学习笔记,希望能和大家一起进步成长💕 目录 💕前言:作者是一名正在学习JAVA的初学者,每天分享自己的学习笔记,希望能和大…

    Python 2023年9月27日
    037
  • 行为识别方法简介

    目录 1 背景介绍 2 方法 2.1 传统有监督特征提取方法 2.1.1 时空关键点(space-time interest points) 2.1.2 密集轨迹(dense-tr…

    Python 2023年10月24日
    060
  • 如何从dataframe中一列含有字母和数组的值中提取其中的数字

    date例子: Price: 2450.000000, C: 618.000000, P: 0.500000, Diff: 617.500000, YSettle_C: 618.0…

    Python 2023年8月16日
    063
  • cuda与torch的安装匹配

    此博客主要用于记录个人的问题解决。 如果能帮到路过的朋友那再好不过啦。 我在某网站的评论所分享的链接下载的torch,似乎是阿里的源。是1.10.1的torch。 pip show…

    Python 2023年9月16日
    086
  • Pytorch/Tensor/Numpy中的广播机制——Broadcast

    根据线性代数的运算规则我们知道,矩阵运算往往都是在两个矩阵维度相同或者相匹配时才能运算。比如加减法需要两个矩阵的维度相同,乘法需要前一个矩阵的列数与后一个矩阵的行数相等。那么在 n…

    Python 2023年8月24日
    039
  • Python – APM 链路跟踪探针之DDtrace

    Max.Bai2021-03 APM (Application Performance Management),也叫链路跟踪系统,比较熟悉的有Pinpoint, SkyWalkin…

    Python 2023年8月13日
    065
  • drf — 限流组件Throttling

    限流组件Throttling 可以对接口访问的频次进行限制,以减轻服务器压力,或者实现特定的业务。一般用于付费购买次数,投票等场景使用. 可选限流类 1.AnonRateThrot…

    Python 2023年6月10日
    0100
  • SVM公式详尽推导,没有思维跳跃。

    假定数据集(T={(x_1,y_1),(x_2,y_2),…,(x_n,y_n)},x_n \in R_k, y_n \in {1,-1}) 线性可分,SVM的优化目标…

    Python 2023年10月21日
    029
  • matplotlib入门(2)

    啊哦~你想找的内容离你而去了哦 内容不存在,可能为如下原因导致: ① 内容还在审核中 ② 内容以前存在,但是由于不符合新 的规定而被删除 ③ 内容地址错误 ④ 作者删除了内容。 可…

    Python 2023年9月5日
    039
  • 跟大佬展开激励讨论String的不可变性

    目录 大佬证明: 我的证明: 后续 什么是不可变? String为什么不可变 为什么要设计成为不可变的呢? 1.首先我们最先可以想到的Java主要做的就是安全 2.其次是字符串常量…

    Python 2023年9月26日
    034
  • 8行代码实现快速排序,简单易懂图解!

    快速排序是一种常用的排序算法,比选择排序快的多。在之前的我随笔中也写过关于快速排序的算法,也可以看一下和现在的区别python实现快速排序 – Mr-Yang – 博客…

    Python 2023年5月23日
    057
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球