【JavaEE】多线程 -- 单例模式

目录

什么是设计模式

设计模式好⽐象棋中的 "棋谱". 红⽅当头炮, ⿊⽅⻢来跳. 针对红⽅的⼀些⾛法, ⿊⽅应招的时候有⼀ 些固定的套路. 按照套路来⾛局势就不会吃亏.

软件开发中也有很多常⻅的 "问题场景". 针对这些问题场景, ⼤佬们总结出了⼀些固定的套路. 按照这 个套路来实现代码, 也不会吃亏.

单例模式

  • 单例模式的概念就是某个类在进程中只能有一个实例对象

为什么只创建一个实例

  • 在实际的开发场景中, 我们很有可能遇到这样一种场景:一组服务器, 包含若干个分片, 存储了100g的数据
  • 后面我们代码中构造了一个DataCent的类, 用类的构造方法创建实例对象, 就会把服务器100g的数据加载到内存中. 这样的对象创建一个还好, 那么如果多创建几个对象, 那么内存就吃不消了. 所以我们写一段代码, 让程序强制我们人只能创建一个对象, 不能创建多个这样的对象. 防止内存吃满造成程序一系列的bug.

饿汉模式和懒汉模式的区别(感性理解)

  • 阿杰和小美是男女朋友关系, 每次吃完饭. 如果是阿杰洗碗, 阿杰就在吃完饭后马上就把全部的碗都洗了. 这就类似于饿汉模式, 没有急着用碗, 也马上把碗洗了.
  • 到了小美洗碗的时候, 小美不是吃完饭就马上洗碗. 而是等了下次吃饭需要碗的时候, 小美才去洗碗. 也就是我们吃饭需要用到碗的时候, 小美才会去洗碗.

这里的碗就是实例的对象, 阿杰和小美是两个线程.

饿汉模式

javascript 复制代码
public class Singleton {
    // 加了 static, 当前的成员成为 类属性, 在类对象上的. 类对象只有一个实例.
    private static Singleton instance = new Singleton();	//在类加载的时候, 就初始化
    public static Singleton getInstance(){
        return instance;
    }
    private Singleton(){    //把实例对象的构造方法设置成私有, 不让类外访问

    }

懒汉模式

  • 类加载的时候不创建实例. 第⼀次使⽤的时候才创建实例.
java 复制代码
public class SingletonLazy {
    private static SingletonLazy instance = null;
    public SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy(); //需要的时候才创建实例
        }
        return instance;
    }
    private SingletonLazy(){

    }
}

线程安全问题

  • 我们但单例模式的唯一一个public方法getInstance, 给别人提供一个实例对象. 那么我们需要考虑的就是在多线程的场景下调用这个方法是否线程安全.

饿汉模式是否安全

  • 先来看, 饿汉模式的getinstance方法
  • 在线程安全的章节中, 我们讲过, 导致多线程出问题的就是对同一个变量进行修改, 并且修改操作的指令不是原子性的.
  • 可是懒汉模式的这里这是返回一个变量, 并没有多变量进行修改操作. 也就不会出现线程安全. 所以饿汉模式天生就是线程安全的.

懒汉模式是否安全

  • 对于懒汉模式, 可以看到我们对于instance变量进行了修改操作. 这个时候在多个线程中, 会对同一个变量进行修改操作, 这个时候修改操作指令不是原子性的. 就会造成线程安全问题了
  • 可以看到在t1线程刚刚执行了判断指令, 就马上调度到t2线程进行创建实例对象操作. 这个时候就算调会t1线程对instance又创建一个实例对象并且赋值. 之前的实例1没有instance引用就会被JVM回收. 但是如果这个实参对象创建的时候向上面的场景一样要100g内存.这种消耗就会特别大. 多来几次服务器就容易崩溃了.

注意: 懒汉模式虽然线程不安全, 但是只是在初始化的时候. 第二次调用的时候因为第一次已经对instance赋值, 所以就会和饿汉模式一样直接返回instance, 只是一个读操作. 后面就线程安全了

  • 根据上面的分析, 知道我们懒汉模式是线程不安全的, 也是由于修改同一个变量, 操作指令不是原子性导致的. 那么对于这种情况我们就要加锁.
  • 可以看到, 如果说我们t1线程在执行完创建对象之前, t2线程因为锁就不能实例对象. t2线程等待t1释放锁后, 发现instance已经不为空, 就会直接返回了. 那么我们就只会创建一个实例, 解决了我们上面对于创建实例对内存开销很大的问题.

不必要加锁导致效率降低

  • 虽然说, 我们通过加锁解决了线程安全的问题. 但是我们上面也已经分析到了. 懒汉模式只是会在第一次初始化的时候才会出现线程安全问题. 第二次调用他的时候就会直接返回了. 所以多线程直接读的方式返回变量,不会进行修改操作, 线程是安全的.
  • 这个时候我们多线程调用的情况下, 还不管三七二十一直接加锁. 针对只是读操作就没必要了, 加了锁另一个线程就会阻塞, 效率就比较低了.

指令重排序导致线程不安全

  • 解决上述问题核心方法是使用 volatile 关键字
  • volatile关键字两个功能:
    • 保证内存可见性, 每次访问变量必须要重新读取内存, 而不会优化到寄存器/缓存中
    • 止使用指令重排序. 针对被 volatile 修饰的变量的读写操作相关指令, 是不能被重排序的.

最终代码

java 复制代码
public class SingletonLazy {
    private static volatile SingletonLazy instance = null;
    private Object locker = new Object();
    public SingletonLazy getInstance(){
        if(instance == null){   //第二次调用的时候就判断到instance不为空
            synchronized(locker){
                if(instance == null){
                    instance = new SingletonLazy(); //需要的时候才创建实例
                }
            }
        }
        return instance;
    }
    private SingletonLazy(){

    }
}