多线程(四)

目录

[一 . 单例模式](#一 . 单例模式)

(1)什么是设计模式?

(2)饿汉模式

(3)懒汉模式

[二 . 指令重排序](#二 . 指令重排序)


今天咱们继续讲解多线程的相关内容

一 . 单例模式

(1)什么是设计模式?

设计模式其实通俗来讲就是一种固定套路,比如打篮球,踢足球,做数学题,下棋等等,在某个特定情况下,我们直接使用一些套路化,公式化的应对措施,就是解决当前问题的最优解,这就是设计模式。

在我们软件开发中也有很多常见的套路,一些 " 问题场景 ",针对这些特定场景,我们的程序员大佬们,前辈们总结了一些固定套路,通过这个套路来公式化的实现代码,就是 " 最优解 "。在某些特定情景下,按照设计模式来写代码,可以使代码不会太差,保证了代码的下限。当然啦,我们如果有能力写出更好的代码,那当然是更好的。

单例模式能保证某个类在程序当中只存在唯一一份实例,而不会创建出多个实例。

Java 中的单例模式具体的实现方式有很多,在我们日常生活中最常见的就是 " 饿汉模式 " 和 " 懒汉模式 ",这是当前的主流写法。

(2)饿汉模式

此处用 static 修饰,这里的 instance 成员变量就变成了 " 类成员 ",它就不再和实例相关而是和类相关了。那么类成员的初始化,就是在 Singleton 这个类被加载的时候,也就相当于咱们程序启动的时候。

在上述代码中,程序一启动,我们的 Singleton 这个类就加载了,类一加载,我们类成员的初始化就完成了,这里的实例创建的非常迫切,也就是我们的 " 饿 ",所以就叫做我们的 " 饿汉模式 "。

但是呢,有了实例还不够,我们还需要在外面对其进行使用,所以我们再在类中提供了一个以 Singleton 为返回值的 getInstance 方法,这样子后续我们需要用到这个实例,我们就可以直接调用这个 getInstance 方法。

最后,我们通过比较 s1、s2 会发现这两个值是一样的,是同一个实例,也就是true。

只要我们不在其它代码中 new 这个类,每次需要使用都通过调用 getInstance 来获取实例,此时这个类就是单例的了。

但是,我们怎样防止别人不来 new 这个类呢?这就是我们单例模式主要需要解决的问题了。

那么就是如图中的样子,咱们再写一个私有的构造方法,这样子在外部就不能再创建新的实例了。

(3)懒汉模式

饿汉模式跟懒汉模式的区别是什么呢?

例如:假设现在我们有一个编译器,需要用它打开一个非常大的,几个G的文件。我们可以使用两种方法来打开文件。

(1)一启动,就把所有的文本文件内容全部一股脑儿的都读取到内存中,然后显示到界面上。这就相当于我们 " 饿汉模式 " 的实现情况。

(2)启动之后,只加载一小部分数据(一个屏幕能显示的最大数据),随着用户进行翻页操作,在按照情况需要,加载剩余的内容。这就相当于我们 " 懒汉模式 " 的实现情况。

当我们首次调用 getInstance,由于此时的 instance 为空,咱们就进入 if 分支,创建实例。后续再重复调用 getInstance 结构都不会创建实例,直接返回。

大家可能会疑惑,为什么这个会有两个 if 语句,并且判断条件还一模一样?这不是多此一举吗?有什么意义呢?大家会发现,我们像下图代码中这样写,也没有任何问题啊?

下面我通过画图给大家分析一下其中可能存在的线程安全隐患:

所以,向我们上述过程中这种 " 先判定,再修改 " 的代码模式,是典型的线程不安全代码,因为我们的判定与修改之间,很可能涉及到线程的切换。

所以,此处我们需要运用 " 锁 " 来解决线程安全问题。提到锁,我们就用 synchornized 的嘛:

是不是这样的呢?答案是否定的,我们需要的是:**让线程在我们执行判定和修改的时候,不去切换线程,所以我们有必要将 " if " 和 " new " 打包成一个整体。**所以应该如下加锁:

这样子就完了吗?其实我们还有一点没有考虑到:虽然我们此处的加锁操作解决了刚刚的线程安全问题,但是与此同时又引入了新的问题 ------ 加锁之后,可能引起阻塞。因为在上述代码中,我们已经 new 完了对象,那么我们的 if 分支就再也进不去了,后续代码的执行,都是单纯的 " 读 " 操作,此时 getInstance 不加锁,线程也是安全的。

而我们当前代码的写法,只要调用 getInstance,都会触发加锁操作,此时虽然没有线程安全问题了,但是加锁的开销是不可忽视的,会加锁产生阻塞,影响到性能。所以我们需要在锁之前再判定一次,而刚好,判定条件也是 " instance == null "。

二 . 指令重排序

上述代码中,还可能存在一个问题:指令重排序的问题。

问题就出在我们的**" instance = new SingletonLazy ( ) ;",这看似是一步,其实至少有三步:**

(1)分配内存空间。

(2)执行构造方法。

(3)将内存空间的地址,赋值给引用变量。

对于这三个指令,编译器在执行的过程中,可能是 1 -> 2 -> 3,也可能是 1 -> 3 -> 2。注意:对于单线程来说,先执行 2 还是先执行 3,本质上是一样的,但是在我们多线程中,线程随时可能切换,这就很可能会造成影响了。(如下图所示:)

那么这个有关指令重排序的问题我们该如何解决呢?再加锁?再加 if 判定?都不是,要想解决这个问题很简单,那就是请出我们的老朋友 ------ volatile :

再之前有关多线程的第二节咱们就聊到过有关 volatile 关键字的用途,详情大家可以移步去看一看,了解了解:多线程(二)-CSDN博客

此时我们加了 volatile 关键字修饰 instance 之后,编译器就会发现,这个变量是 " 易失的 ",围绕这个变量的优化操作就会非常克制。不仅仅是在读取变量的优化上克制,也会在修改变量的优化上克制。加上 volatile 之后,禁止编译器对 instance 赋值操作的指令重排序。

OKK,就聊这么多了,今天主要就是讲解单例模式中的 " 饿汉模式 " 与 " 懒汉模式 ",以及解决 " 懒汉模式 " 中存在的各类线程安全问题。咱们下期再见,与诸君共勉!!!

相关推荐
狂奔的sherry14 小时前
单例模式(巨通俗易懂)普通单例,懒汉单例的实现和区别,依赖注入......
开发语言·c++·单例模式
晨星05272 天前
软件设计模式之单例模式
单例模式·设计模式
code bean4 天前
【wpf】WPF开发避坑指南:单例模式中依赖注入导致XAML设计器崩溃的解决方案
单例模式·wpf
是三好5 天前
单例模式(Singleton Pattern)
java·开发语言·算法·单例模式
青春易逝丶5 天前
单例模式
单例模式
YA3335 天前
java设计模式一、单例模式
java·单例模式·设计模式
枫景Maple6 天前
Unity中多线程与高并发下的单例模式
unity·单例模式·游戏引擎
iiiiaaiashah6 天前
单例模式的mock类注入单元测试与友元类解决方案
java·开发语言·单例模式
jingfeng51410 天前
线程池及线程池单例模式
linux·开发语言·单例模式
Brookty10 天前
深入解析Java并发编程与单例模式
java·开发语言·学习·单例模式·java-ee