本篇会加入个人的所谓鱼式疯言
❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言
而是理解过并总结出来通俗易懂的大白话,
小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.
???可能说的不是那么严谨.但小编初心是能让更多人能接受我们这个概念 !!!
引言
在多线程的世界里,单例模式如同一颗闪耀的明星,它确保了唯一的存在,犹如黑夜中的明灯,指引着程序的正确运行。让我们一同揭开单例模式的神秘面纱,探索它在多线程中的奇妙之处。
目录
单例模式
单例模式的简单实现
单例模式的多线程
一. 单例模式
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
的作用
总结
单例模式: 初步了解了 设计模式 : 软件开发的一种特定的类似 “棋谱”
的一种开发的方案, 并且也认识了 单例模式 : 在全局范围内, 只运行访问的 一个对象的创建对象方案。
单例模式的简单实现: 单例模式的简单实现: 饿汉模式与懒汉模式 。 一般而言,懒汉模式的开发效率比饿汉模式的开发效率更高 。
单例模式的多线程:单例模式下的多线程, 唯一要熟悉的就是懒汉模式的 多线程实现 , 并且结合前面的 synchronized
与 volatile
的配合使用, 解决线程安全问题。
如果觉得小编写的还不错的咱可支持 三连 下 (定有回访哦) , 不妥当的咱请评论区 指正
希望我的文章能给各位宝子们带来哪怕一点点的收获就是 小编创作 的最大 动力 ? ? ?