当前位置:首页 » 《我的小黑屋》 » 正文

【多线程奇妙屋】你听说过设计模式吗?软件开发中可全局访问一个对象的设计模式——单例模式,工作常用, 建议收藏 ! ! !

21 人参与  2024年11月24日 12:01  分类 : 《我的小黑屋》  评论

点击全文阅读


本篇会加入个人的所谓鱼式疯言

❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言

而是理解过并总结出来通俗易懂的大白话,

小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.

???可能说的不是那么严谨.但小编初心是能让更多人能接受我们这个概念 !!!

在这里插入图片描述

引言

在多线程的世界里,单例模式如同一颗闪耀的明星,它确保了唯一的存在,犹如黑夜中的明灯,指引着程序的正确运行。让我们一同揭开单例模式的神秘面纱,探索它在多线程中的奇妙之处。

目录

单例模式

单例模式的简单实现

单例模式的多线程

一. 单例模式

在这里插入图片描述

1. 设计模式的初识

设计模式是在 软件开发 中所常用的一种 实施方案和策略

就相当于小伙伴们在写 算法 时 , 就需要 某种特点的数据结构的对于数据的组织方式 来帮助一样。

可能小伙伴们都在传设计模式有23种, 但是不一定说就只有23种, 只是说常见常用到的设计模式有23种。

设计模式的就好像 棋谱 , 设计模式补齐一些能力不是很出众的程序猿的短板,

并且对于 主流语言是刚刚好的 , 对于一些 不是很主流的语言设计模式也提供更好的支撑

2. 单例模式的初识

小伙伴们是否想过, 声明一个 之后, 是否可以在全局只 使用一个对象(实例) 的方式, 而不能使用 很多个对象(实例) 来实现我们的 业务逻辑

所以才出现了单例模式。

单例模式 : 只允许 实例化一个对象 , 在全局范围内也只能 访问实例化的这一个对象

单例模式的实现方式很多样, 但是主流的实现单例模式的方式主要是以下两种:

饿汉模式 : 先在 类中实例好唯一的一个对象 ,当 用户需要 时就通过方法来访问这 唯一的对象

懒汉模式不在类中实例化对象 , 当用户需要时, 如果 没有实例化好对象就先实例化对象然后访问 ,如果有就 直接访问


初步了解完这 两种模式 ,下面就让小编真正带着小伙伴通过代码的方式来实现吧 ~ ~ ~

二. 单例模式的简单实现

1. 饿汉模式

在这里插入图片描述

<1>. 代码演示

package TestDemo6;// 创建一个单例的类// 饿汉方式实现.// 饿 的意思是 "迫切"// 在类被加载的时候, 就会创建出这个单例的实例class Singleton {    private static Singleton instance = new Singleton();    public static Singleton getInstance() {        return instance;    }    // 单例模式的最关键部分.    private Singleton() { }}public class Demo24 {    public static void main(String[] args) {        Singleton s1 = Singleton.getInstance();        Singleton s2 = Singleton.getInstance();        System.out.println(s1 == s2);    }}

在这里插入图片描述

<2>. 流程阐述

首先声明一个 单例模式的类 然后在这个类中创建一个私有的静态成员变量并且实例化, 这一步是为了准备 用户去调用而提前去实例化对象 。 其次去创建一个方法, 返回上面已经 实例化好的一个对象 , 当外界需要访问的时候, 就通过 getgetInstance() 直接 返回该对象 。 最后把构造方法限定为 private, 不允许用户自己再去实例化对象 , 告诉用户这里只能用已经 实例化的唯一的对象 来使用。

2. 懒汉模式

在这里插入图片描述

<1>. 代码演示

class  SingletonLazy {//    声明一个成员变量    private static  SingletonLazy singletonLazy;    public static SingletonLazy getSingletonLazy() {//        如果该对象没有被实例,还需要使用的话,就需要先是实例化        if (singletonLazy == null) {            singletonLazy = new SingletonLazy();        }        return singletonLazy;    }    // 关键一步,把构造方法私有化    private SingletonLazy() {    }}class Demo25 {    public static void main(String[] args) {//        接收两次        SingletonLazy singletonLazy1 = SingletonLazy.getSingletonLazy();        SingletonLazy singletonLazy2 = SingletonLazy.getSingletonLazy();//        判断两次是否是相同的对象        System.out.println(singletonLazy1 == singletonLazy2);        }}

在这里插入图片描述

<2>. 流程阐述

首先声明一个 SingletonLazy 的类

声明一个私有的 SingletonLazy 的 静态成员变量 , 但 不实例化

当需要 访问对象 时,判断对象是否已经 实例化 , 如果 没有实例化new 出对象并返回。否则就 直接返回 即可。

最终把 构造方法私有化 , 只限制在上述 if 中实例化的那个 唯一的对象

鱼式疯言

无论是 懒汉模式还是饿汉模式 的 构造方法私有化 , 如果是通过常规方式来 再实例化对象是不太可能

如果通过 Java反射机制获取构造方法new对象 是完全有可能的, 而这种在实际生活中,是 不太可能会使用 的, 属于 非常规方式。

3. 饿汉模式与懒汉模式的对比

从上面的实现思路中就可以看出两种实现方式的本质区别了,

饿汉模式: 主要为在 类加载的过程 中就 实例化对象

懒汉模式: 则是在 需要使用对象 时再 实例化对象

那么小伙伴们就可以思考一下,如果我们在实际开发中,如果追求效率的话, 是使用饿汉模式更高效, 还是懒汉模式更高效呢?

答案: 懒汉模式

其实在编程的世界中 ,“懒” 其实是一个褒义词, 越懒就越高效, 就像我们之前学习过的TCP 的 三次握手和四次挥手机制, 明明是四次握手, 但偏偏就要懒一次, 把 中间两次合并成一次来传递 , 虽然懒, 但是这样的 懒人行为正好还提高了效率

所以回到我们这里也是如此, 当需要 单例对象 时,我们才 实例化对象 , 当不需要单例对象时我们就不实例化, 并且当 第一个需要 时, 意味着后面也可能也需要 , 这样就 更好的提高效率

好比现在小编和女神在家吃饭, 一顿三餐 是需要洗碗的。

今天 女神不想洗碗 了, 就轮到小编来持家了

现在关于洗碗,小编有两种策略:

只要 一有脏碗, 我就全部洗干净 , 这就相当于 饿汉模式

把 脏碗都放一起,当哪一顿需要几块碗了,我们就开始洗几块碗 , 这就相当于 懒汉模式

那么我们就可以设想了,一有碗就洗 ,这样 饿汉模式 效率明显很低

但是如果是需要的时候再洗碗,需要多少洗多少 , 就相对而言懒汉模式 效果更高一点

三. 单例模式的多线程

1. 饿汉模式的多线程

其实对于 饿汉模式 来说, 无论是 串行还是并行编程 执行的代码都是大体上是一样的。

  public static Singleton getInstance() {        return instance;    }

小伙伴们都知道对于 串行编程和并行编程 ,最主要考虑的一点就是: 并发编程是否存在 线程安全问题 , 而区别 线程安全问题 的方式,主要是看是否存在含有 写操作

而在我们的 饿汉的单例模式 中, 上述代码中很显然 只存在读操作 , 并没有出现 赋值 这样的 写操作

所以这里的代码就和上面的代码是一样的, 小编在这里就 不赘述 了。

2. 懒汉模式的多线程

<1>. 代码演示

// 懒汉模式实现的单例模式class SingletonLazy {    // 此处先把这个实例的引用设为 null, 先不着急创建实例.    private static  volatile SingletonLazy instance = null;    private static Object locker = new Object();    public static SingletonLazy getInstance() {        // 在这个条件中判定当前是否应该要加锁.        if (instance == null) {            synchronized (locker) {                if (instance == null) {                    instance = new SingletonLazy();                }            }        }        return instance;    }    private SingletonLazy() { }}class Demo25 {   private static SingletonLazy s1;   private static SingletonLazy s2;    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(()->{            s1 = SingletonLazy.getInstance();        });        Thread t2 = new Thread(()->{            s2 = SingletonLazy.getInstance();        });        t1.start();        t2.start();        Thread.sleep(1000);        System.out.println(s1 == s2);    }}

在这里插入图片描述

<2>. 流程阐述

首先,声明一个类 SingletonLazy
  private static  volatile SingletonLazy instance = null;
定义一个私有的修饰的变量, 这里需要注意的一点是: 就是会产生 内存可见性问题 , 为了 防止内存可见性问题 的产生我们就需要加上 volatile 来修饰这个变量。
// 在这个条件中判定当前是否应该要加锁.    synchronized (locker) {        if (instance == null) {            instance = new SingletonLazy();        }    }
由于在懒汉模式中,当第一次使用对象, 就涉及到 if + 赋值 的修改操作, 为了 防止线程不安全问题的发生 , 这里我们就需要把 if + 赋值 操作都打包成 原子 , 于是就使用 synchronized 进行加锁, 保证线程是安全的。
// 在这个条件中判定当前是否应该要加锁.if (instance == null) {    synchronized (locker) {        if (instance == null) {            instance = new SingletonLazy();        }    }}

当第2,3…次使用之后,我们不需要加锁,也就 new 对象 之后, 也就 不需要重新加锁 ,所以我们就需要在 synchronized 的外层再加 一层 if 来判断是否需要加锁 。

将 构造方法私有化(同上)

鱼式疯言

总结说明

volatile 来保证 内存可见性问题 , synchronized 来 解决 线程安全问题 。

这里的 两个if 判断方式相同 ,但是 作用是不一样 的, 外层if 判断 是否需要加锁内层if 判断 是否需要实例化对象

<3>. 加锁操作分析

   if (instance == null) {        instance = new SingletonLazy();    }

是的,对于上面 if + 赋值 来说, 在多线程下的并发执行下是 很容易出现线程安全问题 的。

在这里插入图片描述
如上图,如果 不加锁 的话, 就很有可能会出现 两个线程交互 , 线程一先判断, 接着切换到 另一个线程继续判断。 最终在两个线程中new 了两遍对象。

虽然这里的赋值操作, 只是 更新对象 , 不会重复实例化对象 ,Java垃圾回收机制也会回收原来的那个对象。 但是这里认为程序还是出现了 BUG 了。

所以我们就需要加上 , 将 if 和 修改操作 打包成原子 。 从而解决线程安全问题。

虽然加上了 , 但是小伙伴们有没有思考过,那么是不是每次访问对象是不是都要进行加锁操作呢?

答案: 不是

由于加锁这个操作 本身也是需要消耗性能的 ,同时我们也 不需要让每次访问对象 时, 就进行 加锁操作 , 那么我们就可以在 加锁操作的的外层 再加上 一层 if , 判断一旦对象被实例化了, 就不需要加锁 。

生活栗子:

比如我现在和女神在一起了!

我今天问女神: 你爱不爱我?

女神回答: 爱

我明天问女神: 你爱不爱我?

女神回答: 爱

如果有一天小编进去了 , 这一进去就是一年半载,不知猴年马月的

当我出来的时候, 再问女神你还爱不爱我?

这时的答案就不一定是一样的了。 所以好比 加锁 也是如此,当我们对一段代码进行加锁, 不仅会 损耗一定的性能,同时也含有 一些不确定的因素 会发生。

外层的if 的作用就在于判断 是否需要加锁 。

关于线程安全问题的详解文章,小伙伴们可以多回顾回顾哦~

synchronized 的线程安全问题详解文章链接

<4>. volatile 分析

对于 volatile 的使用, 主要还是提醒 编译器/ JVM 不要对 这个变量的进行优化 , 限制他们优化的程度, 读取到真正修改后的数据 。

在这里插入图片描述

如图线程一实例化对象了, 当线程二再次调用时, 就会 直接跳过实例化对象 的过程, 就会返回 null

内存可见性问题文章详解,小伙伴们如果忘了,记得去回忆一下哦~

内存可见性问题详解文章链接

鱼式疯言

总结概述

volatile 的作用

解决 内存可见性问题 解决 指令重排序的问题

总结

单例模式: 初步了解了 设计模式 : 软件开发的一种特定的类似 “棋谱” 的一种开发的方案, 并且也认识了 单例模式 : 在全局范围内, 只运行访问的 一个对象的创建对象方案。

单例模式的简单实现: 单例模式的简单实现: 饿汉模式与懒汉模式 。 一般而言,懒汉模式的开发效率比饿汉模式的开发效率更高

单例模式的多线程:单例模式下的多线程, 唯一要熟悉的就是懒汉模式的 多线程实现 , 并且结合前面的 synchronizedvolatile 的配合使用, 解决线程安全问题。

如果觉得小编写的还不错的咱可支持 三连 下 (定有回访哦) , 不妥当的咱请评论区 指正

希望我的文章能给各位宝子们带来哪怕一点点的收获就是 小编创作 的最大 动力 ? ? ?

在这里插入图片描述


点击全文阅读


本文链接:http://zhangshiyu.com/post/191267.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1