单例模式

2021-01-25 设计模式架构设计

单例模式同时解决了两个问题, 所以违反了单一职责原则,如今, 单例模式已经变得非常流行, 以至于人们会将只解决下文描述中任意一个问题的东西称为单例。

  1. 保证一个类只有一个实例:为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象,客户端甚至可能没有意识到它们一直都在使用同一个对象。
  2. 为该实例提供一个全局访问节点: 还记得你 (好吧, 其实是我自己) 用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。还有一点: 你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。

# 使用场景

  1. 业务系统全局只需要一个对象实例,比如发号器、redis连接对象等
  2. SpringIOC容器中的bean默认就是单例
  3. SpringBoot 中的controller、service、dao层中通过@autowire的依赖注入对象默认都是单例的
  4. 如果程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式
  5. 如果你需要更加严格地控制全局变量,可以使用单例模式

# 解决方案

所有单例的实现都包含以下两个相同的步骤:

  • 将默认构造函数设为私有, 防止其他对象使用单例类的 new运算符。
  • 新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。

此外,单例的实现还分为两种,懒汉式与饿汉式。

  • 懒汉:就是所谓的懒加载,延迟创建对象
  • 饿汉:与懒汉相反,提前创建对象

# 真实世界类比

政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么, “某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。

# 初探懒汉实现方式

public class SingletonLazy {

    private static SingletonLazy instance;

    /**
     * 单例对象对外开放的方法
     */
    public void process(){
        System.out.println("方法调用成功");
    }

    /**
     * 构造函数私有化
     */
    private SingletonLazy(){}

    /**
     * DCL 双重检查锁定 可以在多线程情况下保持高性能
     *
     * 这是否安全,instance = new SingletonLazy(); 并不是原子性操作
     * 1、分配空间给对象
     * 2、在空间内创建对象
     * 3、将对象赋值给引用instance
     *
     * 假如线程 1-》3-》2顺序,会把值写会主内存,其他线程就会读取到instance最新的值,但是这个是不完全的对象
     * (指令重排)
     * @return
     */
    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

//    /**
//     * 第三种方式,减小锁的力度
//     * @return
//     */
//    public static SingletonLazy getInstance(){
//        if (instance == null){
//            synchronized(SingletonLazy.class){
//                instance = new SingletonLazy();
//            }
//        }
//        return instance;
//    }

//    /**
//     * 第二种
//     * 通过枷锁保证单例 synchronized
//     * 问题:高并发场景下影响效率
//     * @return
//     */
//    public static synchronized SingletonLazy getInstance(){
//        if (instance == null){
//            instance = new SingletonLazy();
//        }
//        return instance;
//    }

//    /**
//     * 第一种实现方式
//     * 对外暴露一个方法获取类的对象
//     *
//     * 线程不安全,多线程下存在安全问题
//     */
//    public static SingletonLazy getInstance(){
//        if (instance == null){
//            instance = new SingletonLazy();
//        }
//        return instance;
//    }
}

调用

public static void main(String[] args) {
    //调用成功
    SingletonLazy.getInstance().process();
    //new SingletonLazy(); 报错,单例模式只允许调用对外开放的方法
}

# 懒汉最终实现+双重检查锁定+内存模型

public class SingletonLazy {

    //volatile是java提供的关键词,可以禁止指令重排
    private static volatile SingletonLazy instance;

    /**
     * 最终实现:双重检查锁定+内存模型
     * @return
     */
    public static SingletonLazy getInstance() {
        //第一重检查
        if (instance == null) {
            //锁定
            synchronized (SingletonLazy.class) {
                //第二重检查
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    
    /**
     * 单例对象对外开放的方法
     */
    public void process() {
        System.out.println("方法调用成功");
    }

    /**
     * 构造函数私有化
     */
    private SingletonLazy() {}

# 饿汉实现

/**
 * Description: 单例设计模式 - 饿汉式实现
 * date: 2021/1/24 21:26
 * @author CV大魔王
 */
public class SingletonHungry {

    private static SingletonHungry instance = new SingletonHungry();

    private SingletonHungry(){}

    public SingletonHungry getInstance(){
        return instance;
    }

    //单例对象调用方法
    public void process(){
        System.out.println("方法调用成功");
    }
}

# 懒汉式和饿汉式的选择

  • 饿汉⽅式:提前创建好对象

    • 优点:实现简单,没有多线程同步问题
    • 缺点:不管有没使⽤,instance对象⼀直占着这段内存
  • 如何选择:

    • 如果对象不⼤,且创建不复杂,直接⽤饿汉的⽅式即可
    • 其他情况则采⽤懒汉实现⽅式
上次更新: 1 年前