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

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

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

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

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

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

在这里插入图片描述

引言

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

目录

  1. 单例模式

  2. 单例模式的简单实现

  3. 单例模式的多线程

一. 单例模式

在这里插入图片描述

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>. 流程阐述

  1. 首先声明一个 单例模式的类
  1. 然后在这个类中创建一个私有的静态成员变量并且实例化, 这一步是为了准备 用户去调用而提前去实例化对象
  1. 其次去创建一个方法, 返回上面已经 实例化好的一个对象 , 当外界需要访问的时候, 就通过 getgetInstance() 直接 返回该对象
  1. 最后把构造方法限定为 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. 只要 一有脏碗, 我就全部洗干净 , 这就相当于 饿汉模式

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

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

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

三. 单例模式的多线程

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>. 流程阐述

  1. 首先,声明一个类 SingletonLazy
  private static  volatile SingletonLazy instance = null;
  1. 定义一个私有的修饰的变量, 这里需要注意的一点是: 就是会产生 内存可见性问题 , 为了 防止内存可见性问题 的产生我们就需要加上 volatile 来修饰这个变量。
// 在这个条件中判定当前是否应该要加锁.

    synchronized (locker) {
        if (instance == null) {
            instance = new SingletonLazy();
        }
    }
  1. 由于在懒汉模式中,当第一次使用对象, 就涉及到 if + 赋值 的修改操作, 为了 防止线程不安全问题的发生 , 这里我们就需要把 if + 赋值 操作都打包成 原子 , 于是就使用 synchronized 进行加锁, 保证线程是安全的
// 在这个条件中判定当前是否应该要加锁.
if (instance == null) {
    synchronized (locker) {
        if (instance == null) {
            instance = new SingletonLazy();
        }
    }
}
  1. 当第2,3…次使用之后,我们不需要加锁,也就 new 对象 之后, 也就 不需要重新加锁 ,所以我们就需要在 synchronized 的外层再加 一层 if 来判断是否需要加锁

  2. 构造方法私有化(同上)

鱼式疯言

总结说明

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 的配合使用, 解决线程安全问题。

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

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

在这里插入图片描述

评论 61
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邂逅岁月

感谢干爹的超能力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值