设计模式之单例 (Singleton) 模式

148 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第18天,点击查看活动详情

简介

单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)

它是设计模式中最简单的形式之一。这一模式的目的是使得类的一个对象成为系统中的唯一实例。要实现这一点,可以从客户端对其进行实例化开始。因此需要用一种只允许生成对象类的唯一实例的机制,“阻止”所有想要生成对象的访问。使用工厂方法来限制实例化过程。这个方法应该是静态方法(类方法),因为让类的实例去生成另一个唯一实例毫无意义。

正章

全局变量的缺点
如果将对象赋值给一个全局变量,那么必须在程序一开始就创建好对象 P170

  • 和 JVM 实现有关,有些 JVM 的实现是:在用到的时候才创建对象 思考题
    Choc-O-Holic 公司使用如下工业强度巧克力锅炉控制器
public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;
    
    public ChocolateBoiler() {
        empty = true;
        boiled = false;
    }
    
    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
            // 在锅炉内填满巧克力和牛奶的混合物
        }
    }
    
    public void drain() {
        if (!isEmpty() && isBoiled()) {
            // 排出煮沸的巧克力和牛奶
            empty = true;
        }
    }
    
    public void boil() {
        if (!isEmpty() && ! isBoiled()) {
            // 将炉内物煮沸
            boiled = true;
        }
    }
    
    public boolean isEmpty() {
        return empty;
    }
    
    public boolnea isBoiled() {
        return boiled;
    }
}

思考题 Choc-O-Holic 公司在有意识地防止不好的事情发生,你不这么认为吗?你可能会担心,如果同时存在两个 ChocolateBoiler(巧克力锅炉)实例,可能将会发生很糟糕的事情。
万一同时有多于一个的 ChocolateBoiler(巧克力锅炉)实例存在,可能发生哪些很糟糕的事呢? P176

  • 由于只有一个物理世界的锅炉,所以如果存在多个实例时,不同实例内的变量可能与物理世界的锅炉情况不对应,造从而成错误的操作。 多线程初始化了两个实例 a 和 b,a 先成功进行 fill() 操作,此时 b 也准备进行 fill() 操作,但由于 b 内的变量没有与物理世界的锅炉情况对应,所以 b 也可以进行 fill() 操作,导致了原料溢出。\

  • 刚开始怎么也想不到会出现什么问题,看了后面单例模式多线程的问题后,仔细思考了一下,只能想到上述可能性。当然,书中忽略了只有一个实例时也存在多线程并发错误的问题(一定程度导致难以想到上述可能性)。 单例模式

image.png

  • Java 1.2 之前,垃圾收集器有个 bug,单例没有全局的引用时会被当作垃圾清楚。Java 1.2 及以后不存在上述问题。 P184 思考题
    所有变量和方法都定义为静态的,直接把类当作一个单例,这样如何? P184

  • 静态初始化的控制权在 Java 手上,这样做可能导致混乱,特别是当有许多类牵涉其中时。 思考题
    多个类加载器有机会创建各自的单例实例,如何避免? P184

  • 自行指定类加载器,并指定同一个类加载器。 单例模式的七种方法
    推荐使用静态内部类和枚举方式

饿汉式 P181

public class Singleton {
    private final static Singleton INSTANCE = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

特点

  • 线程安全
  • 依赖 JVM 类加载机制:JVM 在加载这个类时会马上创建唯一的单例实例 P181 缺点
  • 与全局变量一样:必须在程序一开始就实例化,没有懒加载 P170 饿汉式(变种)
public class Singleton {
    private static Singleton INSTANCE;
    
    static {
        INSTANCE = new Singleton();
    }
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

【扩展】 静态代码块初始化静态变量最好放在定义变量之后,否则会在执行定义变量可能出现被覆盖的问题(如果定义有赋值(包括 null),则会覆盖静态代码块已赋的值)。
原因:静态域的初始化和静态代码块的执行会从上到下依次执行。 如下写法最终会得到 null

public class Singleton {
    static {
        INSTANCE = new Singleton();
    }
    
    private static Singleton INSTANCE = null;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

懒汉式 P176

public class Singleton {
    private static Singleton INSTANCE = null;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

特点

  • 使用时再实例化 缺点
  • 线程不安全 懒汉式(变种) P180
public class Singleton {
    private static Singleton INSTANCE = null;
    
    private Singleton() {}
    
    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

特点 线程安全

  • 使用时再实例化 缺点
  • 效率低 双重校验锁 P182
public class Singleton {
    private volatile static Singleton INSTANCE = null;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

特点

  • 线程安全
  • 使用时再实例化
  • 效率较高
  • volatile 关键字确保:当 INSTANCE 变量杯初始化成 Singleton 实例时,多个线程能正确地处理 INSTANCE 变量 P182
  • 1.4及更早版本会失效,1.5及以后版本适用 P182 静态内部类
public class Singleton {
    private static class SingletonHolder {
        private final static Singleton INSTANCE = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

特点

  • 线程安全
  • 使用时再实例化
  • 依赖 JVM 类加载机制:开始只有 Singleton 被加载了,只有在主动使用 SingletonHolder 时(即调用 getInstance() 时),才会加载 SingletonHolder 类,从而实例化 INSTANCE 枚举
public enum Singleton {
    INSTANCE
}

特点

  • 线程安全
  • 克隆、反射和反序列化均不会破坏单例(上述六种方式都会被破坏)
  • 代码简单
  • 1.5及以后版本才有枚举
  • 初始化就会实例化(反编译后可以发现写法类似饿汉式(变种))