多线程(四)

目录

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

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

相关推荐
码农秋8 小时前
设计模式系列(04):单例模式(Singleton)
单例模式·设计模式
学习使我变快乐1 天前
C++:单例模式
开发语言·c++·单例模式
77tian1 天前
设计模式的原理及深入解析
java·开发语言·单例模式·设计模式·代理模式·享元模式·原型模式
wu~9702 天前
手撕四种常用设计模式(工厂,策略,代理,单例)
java·单例模式·设计模式·代理模式·抽象工厂模式·策略模式
熙客2 天前
创建型:单例模式
单例模式
动感光博3 天前
Unity序列化字段、单例模式(Singleton Pattern)
unity·单例模式·c#
海绵宝宝贾克斯儿3 天前
C++中如何实现一个单例模式?
开发语言·c++·单例模式
史迪仔01123 天前
[python] Python单例模式:__new__与线程安全解析
开发语言·python·单例模式
CGG923 天前
【单例模式】
android·java·单例模式