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