0%

设计模式之构建型模式(三):单例模式

1. 单例模式优点

  • 能够避免对象重复创建,节约空间并提升效率
  • 避免由于操作不同实例导致的逻辑错误

2. 饿汉式

  • 特点:单例变量在声明时就初始化

  • 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
    return instance;
    }
    }
  • 分析

    • 优点:简单直观
    • 缺点:占用内存,增加类初始化时间

3. 懒汉式(一):原教旨

  • 特点:先声明一个空单例变量,需要用时再初始化

  • 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Singleton {

    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
    if (instance == null) {
    instance = new Singleton();
    }
    return instance;
    }
    }
  • 分析

    • 优点:按需加载,避免了内存浪费,减少了类初始化时间
    • 缺点:线程不安全。如果有多个线程同一时间调用 getInstance() 方法,instance 变量可能会被实例化多次

4. 懒汉式(二):线程安全

  • 特点:给判空过程加锁

  • 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Singleton {

    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
    synchronized (Singleton.class) {
    if (instance == null) {
    instance = new Singleton();
    }
    }
    return instance;
    }
    }
  • 分析

    • 优点:能保证多个线程调用 getInstance() 时,一次最多只有一个线程能够执行判空并 new 出实例的操作,所以 instance 只会实例化一次,实现线程安全
    • 缺点:当多个线程调用 getInstance() 时,每次都需要执行 synchronized 同步化方法,严重影响程序的执行效率

5. 懒汉式(三):双重校验锁 + volatile

  • 特点:在同步化之前,再加上一层检查

  • 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class Singleton {

    // TODO: 加 volatile 关键字优化:禁止 JVM 底层指令重排,避免特殊情况下的空指针异常
    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
    if (instance == null) {
    synchronized (Singleton.class) {
    // 注意内部的判空不能去掉,即使外部有了一层判空
    // 因为如果里面不做检查,可能会有两个线程同时通过了外面的判空检查
    // 两个线程分别进入锁中进行 new 操作,导致创建多个实例
    if (instance == null) {
    instance = new Singleton();
    }
    }
    }
    reurn instance;
    }
    }
  • 分析

    • 优点:如果 instance 已经被实例化,则不会执行同步化操作,大大提升了程序效率
    • 缺点:JVM 底层为了优化程序执行效率,可能会对代码进行指令重排,导致在一些特殊情况下出现空指针。为了防止这个问题,更进一步的优化是给 instance 变量加上 volatile 关键字

6. 懒汉式(四):静态内部类

  • 特点:可以实现懒加载,可以保证线程安全

  • 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Singleton {

    private static class SingletonHolder {
    public static Singleton instance = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
    return SingletonHolder.instance;
    }
    }
  • 分析

    • 静态内部类方式是怎么实现懒加载的?

      • Java 类加载过程包括:加载、验证、准备、解析、初始化。初始化阶段即执行类的 clinit 方法(clinit = class + initialize),包括为类的静态变量赋初始值和执行静态代码块中的内容。但不会立即加载内部类,内部类会在使用时才加载。所以当外部类 Singleton 加载时,内部类 SingletonHolder 并不会被立即加载,所以不会像饿汉式那样占用内存
      • Java 虚拟机规定,当访问一个类的静态字段时,如果静态字段所属的类尚未初始化,则立即进行初始化。当调用外部类 Singleton 的 getInstance() 方法时,由于其使用了内部类 SingletonHolder 的静态变量 instance,所以这时才会去初始化内部类 SingletonHolder,在 SingletonHolder 中 new 出 Singleton 对象,这样就实现了懒加载
    • 静态内部类方式是怎么保证线程安全的?

      • Java 虚拟机的设计是非常稳定的,早已考虑到了多线程并发执行的情况。虚拟机在加载类的 clinit 方法时,会保证 clinit 方法在多线程中被正确地加锁、同步
      • 即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行 clinit 方法,其他线程都需要阻塞等待,从而保证了线程安全

7. 饿汉式和懒汉式使用场景的一般建议

  • 饿汉式:构建不复杂,加载完后会立即使用的单例对象
  • 懒汉式:构建过程耗时较长,并不是所有使用此类都会用到的单例对象

8. 双重校验锁单例模式中,volatile 主要用来防止哪几条指令重排序?如果发生了重排序,会导致什么样的错误

  • instance = new Singleton();,在这一行代码中,执行了三条重要指令

    1. 分配对象的内存空间
    2. 初始化对象
    3. 将变量 instance 指向刚分配的内存空间
  • 如果第二条指令第三条指令发生了重排序,可能导致 instance 还未初始化时,其他线程提前通过双重校验锁的外层 null 检查,获取到不为 null,但还没执行初始化的 intance 对象,导致空指针异常

-------------------- 本文结束感谢您的阅读 --------------------