0


多线程——单例模式

·前言

  1. 前面的几篇文章中介绍了多线程编程的基础知识,在本篇文章开始,就会利用前面的多线程编程知识来编写一些代码案例,从而使大家可以更好的理解运用多线程来编写程序,本篇文章会用多线程来实现设计模式中的“单例模式”,这里实现“单例模式”的方式主要介绍两种:“饿汉模式”和“懒汉模式”,下面进行本篇文章的重点内容吧。

一、设计模式

  1. 本篇文章介绍的单例模式属于设计模式中的一种,那么什么是设计模式呢?设计模式和象棋中的“棋谱“”比较类似,比如“红方当头炮,黑方马来跳”,针对红方的一些走法,黑方应招也有一些固定的套路,按照这种套路来下,局势就不会吃亏,按照棋谱下棋,下出来的棋不会太差,因为棋谱会兜住我们下棋的下限,设计模式也是如此,按照设计模式来写代码同样可以兜住我们的下限。
  2. 单例模式,是设计模式的一种,它可以保证某个类在程序中只存在唯一的一份实例,而不会创建出多个实例,这点需求在很多场景都需要,比如在我们前面 MySql 篇章 JDBC 编程中的 DataSource 实例就只需要一个。使用单例模式,就可以对我们的代码进行一个更严格的校验和检查,不会像口头约定那样还可以创建多个实例。
  3. 单例模式的具体实现有很多种,本篇文章就来介绍两种实现方式:“饿汉模式”和“懒汉模式”。

二、饿汉模式

  1. 饿汉模式下实现的单例模式,在类加载时就会创建好对象实例,具体的代码已经运行示例如下所示,通过代码中的注释对代码再进一步介绍:
  1. // 希望这个类在进程中只有一个实例
  2. class Singleton{
  3. private static Singleton instance = new Singleton();
  4. // get 方法设为静态方法,这样其他代码想要使用这个类的实例就需要通过这个方法来获取
  5. // 不应该在其他代码中重新 new 这个对象,而是使用这个方法获取现成的对象
  6. public static Singleton getInstance() {
  7. return instance;
  8. }
  9. // 将构造方法设为 private 这样其他代码中就无法通过构造方法再进行实例化一个新对象
  10. private Singleton() {}
  11. }
  12. public class ThreadDemo1 {
  13. public static void main(String[] args) {
  14. // 利用"饿汉模式"实现的单例模式创建两个对象,观察这两个对象是否相同
  15. Singleton s1 = Singleton.getInstance();
  16. Singleton s2 = Singleton.getInstance();
  17. System.out.println(s1==s2);
  18. }
  19. }

  1. 上述的代码就是“饿汉模式”单例模式中一种简单的实现方式,这里实例是在类加载的时候就创建了,创建的时机非常早,这就相当于程序一启动,实例就创建好了,就使用“饿汉”来形容“创建实例非常迫切,非常早”。

三、懒汉模式

  1. 懒汉模式下实现的单例模式,在类加载的时候不创建实例,在第一次使用的时候才创建实例。这样的设计方式可以节省一些不必要的开销,以生活中的肯德基疯狂星期四为例,只有在星期四时,肯德基的点餐小程序上才会出现疯狂星期四的特价餐品,此时使用懒汉模式,不是星期四时就不会加载疯狂星期四的特价餐品,就会节省一些开销。

1.单线程版

  1. 下面来以懒汉模式来实现一个单线程版的单例模式,示例代码及运行结果如下所示:
  1. // 懒汉模式---单线程版
  2. class SingletonLazy{
  3. // 这个引用指向唯一实例,初始化为 null,而不是立即创建实例
  4. private static SingletonLazy instance = null;
  5. private SingletonLazy() {}
  6. public static SingletonLazy getInstance() {
  7. if (instance == null){
  8. // 首次调用 getInstance 方法,创建实例
  9. instance = new SingletonLazy();
  10. }
  11. // 如果不是第一次调用 getInstance 方法,直接返回之前创建好的实例
  12. return instance;
  13. }
  14. }
  15. public class ThreadDemo2 {
  16. public static void main(String[] args) {
  17. // 利用"懒汉模式"实现的单例模式创建两个对象,观察这两个对象是否相同
  18. SingletonLazy b = SingletonLazy.getInstance();
  19. SingletonLazy b2 = SingletonLazy.getInstance();
  20. System.out.println(b==b2);
  21. }
  22. }

  1. 由运行结果可以看出,上述的代码写法仍然可以保证该类的实例是唯一一个,与此同时,创建实例的时机就不是程序启动时了,而是第一次调用 getInstance 方法的时候。

2.多线程版

  1. 通过上面单线程版的懒汉模式实现单例模式,我们可以来分析一下上述的代码是否是线程安全的呢?结论一定是不安全的,不然也不会再创建一个多线程版的懒汉模式实现单例模式,那么以上代码在哪里会涉及到线程安全问题呢?这里出现问题的核心代码就是 getInstance 方法,下面通过画图的方式来对这里的线程安全问题进行讲解:![](https://i-blog.csdnimg.cn/direct/a2022a755b504594837a734358753dd3.png)
  2. 如上图所示,在线程 t1 判断完成,当前是第一次执行 getInstance 方法后进入 if 语句内,没等创建实例就被调度走去执行线程 t2 ,此时 t2 虽然是第二次调用 getInstance 方法,但是由于线程 t1 调用 getInstance 方法还没有创建实例,所以线程 t2 执行 if 语句显示 instance 仍然为 null,此时线程 t2 开始创建实例,并返回实例,然后又跳转回线程 t1 t1 继续执行创建实例,这时,该进程中就会出现两个实例,也就出现了线程安全问题。
  3. 如何改进单线程的懒汉模式,使它也成为线程安全的代码呢?这就需要我们进行加锁操作,想要使这里的代码执行正确,其实只需把 if 创建实例的两个操作打包成原子的(不可拆分),这样就可以解决单线程的懒汉模式中的线程安全的问题,加锁逻辑如下图所示:![](https://i-blog.csdnimg.cn/direct/3e9baea71f134e7dab785868bdfe4541.png)
  4. 如上图两个线程在加锁后的执行流程所示,此时就可以确保,一定是 t1 执行完实例(new)操作修改了 instance 之后再回到 t2 执行 if 语句了,这时 if 的条件就不会成立了,t2 就会直接返回 instance 了。
  5. 但是这样加锁之后还有一个问题,如果 instance 已经创建过实例了,此时后续再调用 getInstance 方法就都是直接返回 instance 实例了,这时调用 getInstance 方法就属于纯粹的读操作了,就不会有线程安全问题了,不过,按照上图中的代码逻辑,即使创建完 instance 实例后是线程安全的代码,仍然每次调用都会先加锁再释放锁,此时效率就会变低(加锁意味着产生阻塞,一旦阻塞解除时间就不确定了)。
  6. 为了解决上述加锁引入的新问题,我们可以在每次加锁前再进行一次判断,仍然是判断当前 instance 的值是否为 null ,为 null 就继续加锁,不为 null 就可以直接返回 instance 对象,不用再进行加锁操作了,具体代码如下图所示:![](https://i-blog.csdnimg.cn/direct/a24edfb3016e420d91e56ab1c63492f7.png)
  7. 如上图所示的代码中,synchronized 上下两条 if 语句中判断的内容是一样的,这里虽然 if 中进行的判断相同,但是所判断的含义还是有所差别:
  1. 第一个 if 判断当前是否要加锁;
  2. 第二个 if 判断的是当前是否要创建实例
  1. 上面代码很凑巧的 if 中的判断条件相同了,但是一个是为了保证“线程安全”一个是“保证“执行效率”,这也就形成了双重校验锁。
  2. 代码改到此处,还是存在一个问题,那就是由指令重排序引起的线程安全问题,指令重排序是一种编译器的优化方式,调整原有的代码执行顺序,保证逻辑不变的前提下提高程序的效率,但是在多线程中,这种优化就很可能带来线程安全问题,上面代码中,创建 instance 实例的过程就很可能会被指令重排序,创建 instance 实例代码如下:
  1. instance = new SingletonLazy();
  1. 上面这段代码,可以拆分成三个大的步骤:
  1. 申请一段内存空间;
  2. 在这个内存空间上调用构造方法,创建出这个实例;
  3. 把这个内存地址赋值给 instance 引用变量。
  1. 正常的情况下,会按 1,2,3 的顺序来执行上面这段代码,但是编译器可能会将上面代码优化成 1,3,2 的顺序来执行,这时就可能会出现问题,如下图所示的情况: ![](https://i-blog.csdnimg.cn/direct/35c3c5fce7fc46e9a4a0f0c11f6ccd57.png)
  2. 如上图的线程调度过程,t2 线程执行完 getInstance 方法后得到的是一个各个属性都未初始化“全0”值的 instance 实例,此时如果使用 t2 线程如果使用了 instance 里面的属性或者方法就会出现错误。
  3. 这种错误出现的原因是由于线程 t1 在创建实例执行完了 1,3 后,被调度走,此时 instance 指向的是一个非 null 的,但是未初始化的对象,这时 t2 线程就会判定 instance==null 不成立,直接 return ,得到一个各个属性都未初始“全0”值的 instance 实例,此时使用这个实例就会出现问题,但是如果创建实例的代码按照 1,2,3 的顺序来执行,就不会出现上述的问题了,所以解决这个问题的方法就是阻止编译器对这段代码的指令重排序,这就需要使用到我们前面文章介绍的关键字 volatile 了。
  4. 这里还是再介绍一下 volatile 关键字的功能把,主要有两个:
  1. 保证内存可见性:每次访问变量都必须要重新读取内存,而不会优化成到寄存器或缓存中读取变量;
  2. 禁止指令重排序:针对这个 volatile 关键字修饰的变量的读写操作相关指令是不能被重排序的。
  1. 代码中需要进行指令重排序的地方是为 instance 创建实例的时候,所以我们可以直接针对这个变量加上 volatile 关键字进行修饰,这样,针对这个变量再进行读写操作就不会出现重排序了,此时,创建实例的顺序一定是 1,2,3 也就预防了上述的问题。
  2. 代码修改到这里就算没有问题了,那么正确懒汉模式实现单例模式多线程版的代码就可以写出来了,代码及一些详细注释如下所示:
  1. // 懒汉模式---多线程版
  2. class SingletonLazy{
  3. // 这个引用指向唯一实例,初始化为 null,而不是立即创建实例
  4. private volatile static SingletonLazy instance = null;
  5. private static Object locker = new Object();
  6. private SingletonLazy() {}
  7. public static SingletonLazy getInstance() {
  8. // 如果 instance 为 null, 说明是首次调用,首次调用就需要考虑线程安全问题,需要加锁
  9. if (instance == null) {
  10. synchronized (locker) {
  11. if (instance == null){
  12. // 首次调用 getInstance 方法,创建实例
  13. instance = new SingletonLazy();
  14. }
  15. }
  16. }
  17. // 如果不是第一次调用 getInstance 方法,直接返回之前创建好的实例
  18. return instance;
  19. }
  20. }
  21. public class ThreadDemo2 {
  22. public static void main(String[] args) {
  23. // 利用"懒汉模式"实现的单例模式创建两个对象,观察这两个对象是否相同
  24. SingletonLazy b = SingletonLazy.getInstance();
  25. SingletonLazy b2 = SingletonLazy.getInstance();
  26. System.out.println(b==b2);
  27. }
  28. }

·结尾

  1. 文章到这里就要结束了,本篇文章利用前面文章介绍的多线程基础知识来实现了一个小案例——单例模式的实现,这里介绍的两种实现方式:饿汉模式与懒汉模式,由于饿汉模式从类加载时就已经创建好实例,后续获取实例都是读操作不涉及线程安全问题,所以饿汉模式下的单例模式代码天生就是线程安全的,反观,懒汉模式在多线程与单线程下就有很大的差别了,此时单线程版的懒汉模式在多线程中就会引发线程安全问题,上面文章详细介绍了每个会出现线程安全问题的地方,希望能够给大家讲解清楚,最后在基于单线程版的懒汉模式代码下,修改出了多线程版的懒汉模式代码,理解清楚这里相信会对你理解线程安全问题有很大的帮助,如果对文章哪里感到疑惑,欢迎在评论区进行留言讨论哦~我们下一篇文章再见~~~

本文转载自: https://blog.csdn.net/HKJ_numb1/article/details/142963625
版权归原作者 学者yyds 所有, 如有侵权,请联系我们删除。

“多线程——单例模式”的评论:

还没有评论