当前位置: 首页 > news >正文

多线程——单例模式

目录

1.设计模式 - 单例模式

2.饿汉模式

3.懒汉模式

3.1 初始版-非线程安全

3.2 synchronized 修饰

3.3 双重 if

3.4 volatile 修饰

4.小结


经过几期的多线程学习,已经对多线程有了一定的了解。从这期开始,将会对多线程的使用做一个更深入的探讨。


1.设计模式 - 单例模式

设计模式是一个抽象的概念,是指软件设计中针对常见问题的可重用解决方案。

大家都一定听说过高斯计算从 1 加到 100 的故事,有一天,高斯的数学老师布置了一道从1加到100的习题,想让学生们算一整节课,结果刚说完题目高斯就报出了答案。 原来,他发现数列两头可以一 一配对:1+1002+99……每一对的和都是101,共有50对,所以总和是5050。后面这个问题就逐渐的演变为我们今天熟悉的等差数列。我们对等差数列并不陌生,但是如果前人发现这个规律,可能我们一时间也不会想起来这种规律,属于是站在巨人的肩膀上学习了。

设计模式就是这样的场景,在计算机中,一些大佬们会针对一些常用典型的场景,为了避免重复造轮子,就设计出了对应的典型的解决方案,这就是设计模式,相当于公式化,后人在使用时直接”套公式“,就不会让这种问题的解决方案差到哪里去。单例模式就属于典型的设计模式之一。

本期主要讲的就是单例模式。单例模式是运行程序中的某个类只有一个实例,也就是只会 new 一次对象。比如一个学校只有一位正校长,一位书记,但是副校长可以有很多人,人体也只有一颗心脏。单例模式可以确保实例唯一、只会创建一次对象节省资源等。

单例模式最常见的有“饿汉模式”和“懒汉模式”,下面将逐一介绍这两种模式。

2.饿汉模式

先从字面来理解,我们什么时候会形容一个人是“饿汉”?肯定是看到一个人在吃饭时狼吞虎咽,恨不得几口饭当一口饭吃的感觉,如果是这种情况,说明这个人可能非常“饥饿”。

在计算机单例模式中,如果一个类在加载时就创建了实例,就叫“饿汉模式”。这种模式因其“急切”初始化的特性而命名为“饿汉”,因为只要程序一运行就创建了,都不知道会不会使用它。

怎么实现“饿汉模式”?首先要抓住“急切”的特性,既然是类在加载时就创了实例,那就说明不论是否需要这个类的实例,只要该类加载就会创建都要创建,并且在外部不能再对其实例化。先来看代码:

class Singleton{private static Singleton instance = new Singleton();public static Singleton getInstance(){return instance;}private Singleton() {}
}
public class Single1 {public static void main(String[] args) {}
}

代码解读:主要看 class Singleton 这个类

  • 首先定义这个类的静态成员 instance静态变量初始化在类初始化阶段完成,早于任何线程访问,类加载往往是程序一启动就触发,也就是说程序一启动,这个实例就被创建了
  • 提供 getInstance 方法当线程想要使用这个实例时,可以通过这个方法获取获取实例,如果多线程一起调用,它们得到的对象是一样的,因为定义 instance 时就已经实例化,,不会出现竞争的情况,这里也说明了“饿汉模式”是天然线程安全的
  • 构造方法,这是单例模式的点睛之笔,在以往学习任何一个类的构造方法时,查看源码都可以看见这些构造方法是被 public 修饰,而我们设计单例模式时,构造方法必须用 private 修饰,这就使得这个类将不再被实例化

测试:

测试1中,定义两个 Singleton 变量,用 == 判断它们是否相等,根据结果可以看到返回的是 true,说明它们得到的对象是相等的,当我们查看哈希值时,哈希值是相等的,这就说明了这两个变量实际指向的是同一个对象。

测试2中,我们尝试实例化 Singleton 对象,但是编译失败,这就是 private 修饰构造方法的点睛之笔。

当然,这个代码实现实例化的对象是无参的,如果想要传参数,那么在一开始定义静态成员 instance 实例化的时候就可以传参数进去,这里不再演示。

果还记得反射这一知识点,可能会有的小伙伴认为这个模式可以被反射攻击。确实会的,比如以以下是通过反射的方式尝试进行修改,导致结果是 false,并且哈希值也不一样:

怎么防御反射攻击?在构造方法里抛一个异常:

class Singleton {private static final Singleton instance = new Singleton();private Singleton() {// 防止反射攻击if (instance != null) {throw new RuntimeException("单例模式禁止通过反射创建实例");}}public static Singleton getInstance() {return instance;}
}

因为“饿汉模式”是类加载时就已经创建实例,而反射攻击时在这个阶段之后:

Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton illegalInstance = constructor.newInstance(); // 这里会抛出异常

这里就相当于有一个时间差,即类加载初始化在反射调用之前,可以确保反射调用时 instance 就已经存在,并且 instance 被 final 修饰,可以确保实例引用时不会被反射修改。

这只是简单的防御措施,针对高强度攻击依然无法防御,这里不过多深究,本期主要是了解单例模式的设计。

3.懒汉模式

3.1 初始版-非线程安全

有了饿汉模式的基础,其实懒汉就不难理解了。“懒汉”则说明它非常懒,喜欢“摆烂”,只要被催促的时候才会行动起来。

在计算机单例模式中,一个类只有在第一次请求时才会被创建,叫做懒汉模式。

我们先仿照“饿汉模式”的代码,把“懒汉模式”的代码整体框架写出来(这个一个初始版本):

class SingletonLazy {private static SingletonLazy instance;private SingletonLazy() {}public static SingletonLazy getInstance() {if (instance == null) {instance = new SingletonLazy(); }return instance;}
}public class Single2 {public static void main(String[] args) {}
}

代码解读

  • 首先定义这个类的静态成员 instance,因为是只有被需要的时候才会被创建实例,所以这里不用 final 修饰,同时也不进行初始化,默认为 null
  • 提供 getInstance 方法当线程想要使用这个实例时,可以提供这个获取获取实例,由于定义的时候并没有进行初始化,默认为 null,所以在这里调用这个方法的时候,要先进行判断是否而空(可能多次调用,单例模式只有一个实例,不能多次创建),是空就实例化,后续再调用时就不会再进行实例化。
  • 构造方法,这是单例模式的点睛之笔,在以往学习任何一个类的构造方法时,查看源码都可以看见这些构造方法是被 public 修饰,而我们设计单例模式时,构造方法必须用 private 修饰,这就使得这个类将不再被实例化

3.2 synchronized 修饰

与“饿汉模式”不同的是,“懒汉模式”的实例化是在 getInstance 方法里,这里有 new 和 == ,涉及到修改操作,是非原子的,而单例模式下实例只能有一份,所以这种方式存在线程安全问题。如何解决?

对于非原子的线程安全问题,可以进行加锁 synchronizedsynchronized 一是可以放在方法中直接修饰 getInstance二是可以放在 if 判断条件的外面(如果把 synchronized 放在 if 判断里,是起不到作用的,因为涉及到的非原子问题主要是 == 导致)。这里以第二种方式实现(为什么不用第一种,后文会介绍)

class SingletonLazy {private static SingletonLazy instance;private static  Object locker = new Object();//锁private SingletonLazy() {}public static SingletonLazy getInstance() {synchronized (locker) {if (instance == null) {instance = new SingletonLazy();}return instance;}}
}public class Single2 {public static void main(String[] args) {}
}

3.3 双重 if

我们知道加锁是为了让一个线程阻塞等待持有锁的线程先执行。但是,上面展示的代码,如果每次希望调用一次  getInstance 方法,是不是意味着每次都要有加锁和释放锁的时间等待,对于计算来讲,这个时间是意味着很久的,在多线程下,这种加锁会造成相互堵塞,影响了程序的运行效率。

怎么解决这个问题?按需加锁。按需加锁就是有需要的时候再加锁,涉及线程安全问题时再加锁,不涉及就不再加锁,而什么情况下涉及线程安全问题?就是在 instance 为 null 的时候,这个判断 == 以及在 new 对象的时候,会涉及到线程安全问题。当 instance 不为 null 的时候,是不是就意味着不需要加锁了?所以,这个时候我们 synchronized 的外层再用一个 if 条件判断 instance 是不是为 null,如果是,就加锁,如果不是,就不加锁,这就是双重 if

class SingletonLazy {private static SingletonLazy instance;private static  Object locker = new Object();//锁private SingletonLazy() {}public static SingletonLazy getInstance() {if (instance == null) {//第一次ifsynchronized (locker) {if (instance == null) {//第二次ifinstance = new SingletonLazy();}}}return instance;}
}

这两个 if 的作用不同:第一个 if 是判断 instance 是否为空,如果为空才加锁,并创建实例,如果不为空,那么整个第一个 if 的代码块都不再执行,减少了加锁和释放锁的时间,第二个 if 也是判断 instance 是否为空,但这里的目的是创建实例,在锁内。

3.4 volatile 修饰

现在,上面的版本已经解决了一部分的线程安全问题。既然说一部分,那么就说明线程安全还没有达到预期(这里只考虑单例模式下“懒汉模式”的设计,不再考虑反射或者其它情况的攻击)。

我们说造成线程安全问题的原因主要有五条:

  1. 操作系统对线程的调度是随机的、抢占式执行的(根本原因)
  2. 多个线程同时修改同一个变量
  3. 修改变量的操作不是原子的
  4. 内存可见性问题
  5. 指令重排序问题

第1条是根本原因,我们无法改变,第2条在多线程情况下我们有时候就希望改变同一个变量,也无法改变,现在第3条已经被我们解决了,第4条和第5条用我们的角度看待解决方式是一样的。接下来分析一下会不会出现第4条和第5条的线程安全问题情况。

如果有两个线程,线程1在读取 instance 时,线程2会不会已经对它进行修改而线程1不知道呢?这肯定会存在的,因为线程1在读取时,可能判断 instance == null 条件是成立的,这会导致线程2的修改无法及时将修改的共享变量返回主内存,导致线程1再创建一个实例,这是会发生错误的,所以这里的“内存可见性问题”取决于编译器的优化,为了稳妥起见,我们可以用 volatile 修饰。

但不是说用 volatile 修饰 instance 后,就不考虑指令重排序的问题,因为这两个问题的解决方案是一致的。接下来分析一下指令重排序的情况:

我们直到指令重排序也是编译器优化的一种提现方式,会在保证逻辑不变的前提下,调整代码的执行顺序以达到提升性能的效果。但是在实例化对象的改成中,会涉及到三个步骤:1)申请内存空间;2)在这个内存空间上构造对象(初始化);3)将引用赋值给 instance (这个时候 instance 不再是 null)。但在编译器的优化下,如果发生指令重排序,就有可能把顺序调整成①③②,这在单线程条件下不需要担心,但在多线程情况下,如果线程发生顺序调整,就会出现bug,错误时间点如下:

所以,在这种情况下,就需要用 volatile 修饰 instance,而不是简单的“内存可见”问题,这里的 volatile 主要解决的问题就是“指令重排序”问题。

class SingletonLazy {private volatile static SingletonLazy instance;private static  Object locker = new Object();//锁private SingletonLazy() {}public static SingletonLazy getInstance() {if (instance == null) {//第一次ifsynchronized (locker) {if (instance == null) {//第二次ifinstance = new SingletonLazy();}}}return instance;}
}

这样的版本才是真正完成了“懒汉模式”的线程安全问题。关于测试,可以和“饿汉模式”类似,这里也不再进行演示。

4.小结

设计模式是指软件设计中针对常见问题的可重用解决方案。单例模式就属于典型的设计模式之一。

单例模式最常见的有“饿汉模式”和“懒汉模式”。

如果一个类在加载时就创建了实例,就叫“饿汉模式”;一个类只有在第一次请求时才会被创建,叫做“懒汉模式”。

“懒汉模式”需要注意线程安全问题和双重 if 的含义。

“饿汉模式”和“懒汉模式”各有优点,比如“饿汉模式”最简单,在类加载时就创建,可以认为天然线程安全,但这种可能造成一定的资源浪费,而“懒汉模式:比较复杂,需要考虑多种情况下才能避免线程安全问题,虽然有双重 if ,但只有在被使用时才会被创建实例,可以认为比”饿汉模式“节省一定的资源。


单例模式实现的方式有很多,最常见的就是本期介绍的“饿汉模式”和懒汉“模式”。下期将介绍设计模式的第二种模式:阻塞队列。我们知道队列是一种先进先出的数据结构,那阻塞队列是什么?队列为什么会发生阻塞?欲知后事如何,且听下回分解!

http://www.dtcms.com/a/403063.html

相关文章:

  • 镜头调焦的 调整的是焦距还是像距?
  • (四)React+.Net+Typescript全栈(错误处理)
  • @ant-design/icons-vue 打包成多格式库
  • 什么是营销型网站?杭州建设教育网站
  • C++开发环境(VSCode + CMake + gdb)
  • JAVA CodeX精选实用代码示例
  • 肥东网站建设南京医院网站建设
  • Qt 多线程解析
  • ZooKeeper与Kafka分布式:从基础原理到集群部署
  • 免费网站服务器安全软件下载wordpress权限设置方法
  • three.js射线拾取点击位置与屏幕坐标映射
  • AutoMQ × Ververica:打造云原生实时数据流最佳实践!
  • Laravel5.8 使用 snappyPDF 生成PDF文件
  • 自己做网站的图片手机芒果tv2016旧版
  • L4 vs L7 负载均衡:彻底理解、对比与实战指南
  • wordpress站群软件自己的网站怎么赚钱
  • 零知IDE——基于STM32F407VET6和MCP2515实现CAN通信与数据采集
  • 若依框架-Spring Boot
  • 全新 CloudPilot AI:嵌入 Kubernetes 的 SRE Agent,降本与韧性双提升!
  • 自建网站推广的最新发展wordpress同步到报价号
  • 4、导线、端子及印制电路板元器件的插装、焊接及拆焊
  • 【Java八股文】13-中间件面试篇
  • (四)优雅重构:洞悉“搬移特性”的艺术与实践
  • 网站建设专用图形库商务网站建设方案
  • 快速入门HarmonyOS应用开发(三)
  • Easysearch 国产替代 Elasticsearch:8 大核心问题解读
  • 【机器学习】搭建对抗神经网络模型来实现 MNIST 手写数字生成
  • 做推广的网站那个好中国机房建设公司排名
  • odoo18应用、队列服务器分离(SSHFS)
  • 老年健康管理小工具抖音快手微信小程序看广告流量主开源