【多线程初阶】单例模式 & 指令重排序问题

发布于:2025-06-07 ⋅ 阅读:(21) ⋅ 点赞:(0)

1.单例模式

单例模式确保某个类在程序中只有一个实例,避免多次创建实例(禁止多次使用 new),要实现这一点的关键在于将类的所有构造方法声明为private,这样类外无法直接访问构造方法, new操作会编译时报错,从而保证类的实例的唯一性

只有一个实例,这样的要求,开发中是常见的需求场景,比如MySQL中的JDBC编程的第一步 DataSource(描述了数据库服务器在哪里,URL,user,password),这样的场景非常适合于作为单例,描述数据库的信息,类似于存储数据库信息这样的对象,由于数据库只有一份,即使搞多个这样的对象,也没啥意义,也是一样的信息

单例模式的实现方式主要有两种:饿汉方式 和 懒汉方式

1)饿汉模式

类加载的同时,创建实例

//通过饿汉模式构造单例模式
    class Singleton{
        private static  Singleton instance = new Singleton();

        public static  Singleton getInstance(){
            return instance;
        }

        private Singleton(){
            
        }

}
public class Demo27 {
    public static void main(String[] args) {
        Singleton t1 = Singleton.getInstance();
        Singleton t2 = Singleton.getInstance();
        System.out.println(t1 == t2);
       // Singleton t3 = new Singleton();
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2)懒汉模式

  • 饿 是尽量早的创建实例
  • 懒 是尽量晚的创建实例(甚至可能不创建了) 延迟创建
  • 懒的另一个含义 高效率~~
  • 如果说,饿汉模式是在类加载的时候(一个比较早的时期),进行创建实例,并且使用private修饰所有的构造方法,使得代码中无法创建该类的其他实例
  • 那么懒汉方式的核心思路,就是延迟创建实例,真正用到实例,再去创建,甚至可能不创建实例,这样可以减小开销,提升效率

①.单线程版本

class  SingletonLazy{
    //懒汉模式-单线程
    private  static  SingletonLazy instance = null;
    public  static  SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
    private  SingletonLazy(){

    }
}

在这里插入图片描述

②.多线程版本

class  SingletonLazy{
    private  static  SingletonLazy instance = null;
    private  static  Object locker = new Object();
    public static  SingletonLazy getInstance(){
        if(instance == null){
            synchronized (locker){
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy(){

    }
}
public class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);

    }
}

在这里插入图片描述

2.分析单例模式里的线程安全问题

在这里插入图片描述

刚才编写的两份代码(饿汉和懒汉),是否是线程安全的?如果不安全如何解决问题?–这两个模式下的getInstance在多线程环境下调用,是否会出bug~

1)饿汉模式

在这里插入图片描述

2)懒汉模式

单线程版本的懒汉模式,如果在多线程环境下运行会出现什么问题?

在这里插入图片描述

在这里插入图片描述

  • instance 被 static 修饰说明这个一块内存,却被多个线程调用getInstance(),同一块空间被修改多次,并且该赋值操作不是原子的
  • getInstance()方法,不仅有读操作,还有写操作(满足if条件才赋值,不满足if条件不会赋值),所以判断条件和赋值这两个操作是密不可分的,就是因为无法保证这两步操作无法紧密执行,就会出现线程安全问题

懒汉模式是如何出现线程安全问题的

要了解这个问题,我们可以通过画时间轴来更直观的感受
在这里插入图片描述

  • ①.判断条件确实为NULL,OK那么线程1继续向下执行,不会跳出if代码块
  • ②.判断条件确实为NULL,OK那么线程2继续向下执行,不会跳出if代码块
  • ③.线程1可以继续向下执行,执行到new了一个对象,然后return
  • ④.线程2可以继续向下执行,执行到new了一个对象,然后return
  • 这里就出现了一个很大的问题 ! !

随着线程2的创建实例,这个操作覆盖掉了,线程1new出来的对象,线程1new出的对象被GC给释放掉了~~
第一次new这个对象的时候,是会进行加载数据的,有可能我们的数据达到100G,100G的数据从硬盘加载到内存 大概要十分钟
本来只需要十分钟,可以由于上述BUG,加载了两份,导致我们的启动时间逼近20分钟
这就与我们的预期不符,妥妥的BUG

3.解决问题

那我们如何解决这个线程安全问题呢? 常规方法:加锁!
在这里插入图片描述

  • 显然不行,不是加了synchronized就会线程安全的
  • 我们要具体的分析具体代码
  • 前面分析过了,是判断条件和赋值操作,这两个操作一起决定的代码在多线程环境下是非原子操作
  • 也就是说,修改是原子的,但是此处为"条件修改"
  • 我们希望,条件判断和修改打包成原子操作

在这里插入图片描述

  • 引入synchronized锁之后,后执行的线程就会在加锁的位置阻塞,阻塞到前一个线程解锁
  • 当后一个线程进入判断条件时,前一个线程已经修改完毕,instance不再为NULL,就不会执行后续的new操作
  • 后续再调用getInstance,此时都是直接执行 return
  • if + return 就是纯粹的读操作,读操作不涉及线程安全问题

进一步优化

加锁导致的执行效率优化

  • 虽然不涉及线程安全问题了,但是每次调用上述getInstance方法,都会触发一次加锁操作
  • 多线程情况下,这里的加锁,就是相互阻塞,影响程序的执行效率
  • 一旦阻塞,此时对于计算机来说,阻塞的时间间隔,就是"沧海桑田",不知道啥时候才能调度
  • 我们应该按需加锁,真正涉及到线程安全的时候,再加锁,不涉及,就不加
  • 加锁时机:若实例已经创建过了,就不涉及线程安全问题,没创建,就涉及线程安全问题

在这里插入图片描述
在这里插入图片描述

  • 以往都是"单线程"程序,单线程中.连续两个相同的 if ,是无意义的
  • 单线程中,执行流是只有一个的 ,上一个if的判断结果和下一个if的是一样的
  • 多线程中,两次判定之间,可能存在其他线程就把 if 中的 instance变量修改了 也导致这里的两次if的结论可能不同

预防内存可见性问题

  • 是否会存在内存可见性问题?
  • 可能会存在,编译器优化这个事情,非常复杂
  • 编译器优化往往不只是 javac自己的工作,通常是javac和jvm配合的效果(甚至是操作系统也要配合)
  • 为了稳妥起见,可以给instance直接加上一个volatile,从根本上杜绝,内存可见性问题
private  static volatile SingletonLazy instance = null;

4.解决指令重排序问题

  • 这里更关键的问题,是指令重排序导致的线程安全问题
  • 指令重排序也是编译器优化的一种体现形式,编译会在逻辑不变的前提下,调整代码的执行顺序,来达到提升性能的效果

举个栗子~~
比如,我们去买菜,我们买西红柿,鸡蛋,茄子,黄瓜
在这里插入图片描述

在这里插入图片描述
第二幅图的执行顺序,明显效率高多了

在这里插入图片描述

比如,我们生活中会出现的指令重排序情况

  • 1.买房 2. 装修 3.拿到钥匙
  • 1.买房 3.拿到钥匙 2.装修

在这里插入图片描述

  • volatile 的功能有两方面
  • 1.确保每次读取操作,都是读内存 -->确保内存可见性
  • 2.关于该变量的读取和修改操作,不会触发重排序 -->避免指令重排序问题

网站公告

今日签到

点亮在社区的每一天
去签到