设计模式之单例模式

1、什么是单例模式

​ 单例模式是指保证某个类在整个软件系统中只有一个对象实例,并且该类仅提供一个返回其对象实例的方法(通常为静态方法)

2、单例模式的种类

​ 经典的单例模式实现方式一般有五种:

2.1 饿汉式

// ①饿汉式:使用静态常量
static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    // 2.类的内部创建对象
    private final static Singleton instance = new Singleton();
    // 3.向外部暴露一个静态的公共方法
    public static Singleton getInstance() {
        return instance;
    }
}
// ②饿汉式:使用静态代码块
static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    private static final Singleton instance;
    // 2.静态代码块实例化
    static {
        instance = new Singleton();
    }
    // 3.向外部暴露一个静态的公共方法
    public static Singleton getInstance() {
        return instance;
    }
}

饿汉式顾名思义就是迫不及待地加载该类的对象实例,对象实例的加载最早是在类的加载过程中的初始化阶段(即静态引用变量的加载,对应字节码文件中<clinit>方法的执行),加载过程由JVM保证线程安全。饿汉式会浪费内存,但是随着计算机的发展,内存已经不是问题了,所以使用饿汉式也未尝不可。

​ JDK源码举例:

​ 该类位于java.lang包下,首先将构造方法私有化,声明了一个私有的静态变量并且对该变量进行对象实例的创建,再创建一个公有的静态方法返回这个对象实例,这是比较常用的一种实现单例模式的方式。

2.2 懒汉式

// ①懒汉式:线程不安全
static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    private static Singleton instance;
    // 2.向外部暴露一个静态的公共方法
    public static Singleton getInstance() {
        // 3.instance == null时进行实例化
        if ( instance == null ) {
            // new Singleton()不是一个原子操作,JVM中会进行大致[创建对象-分配内存-对象初始化]等过程,在这之前instance都为null
            // 多线程情况下,多个线程同时执行到该位置,线程获取到时间片后会继续执行,就可能创建多个实例
            instance = new Singleton();
        }
        return instance;
    }
}
// ②懒汉式:线程安全(方法上添加 synchronized 关键字)
static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    private static Singleton instance;
    // 2.向外部暴露一个静态的公共方法, synchronized 保证线程安全
    public static synchronized Singleton getInstance() {
        // 3.instance == null时进行实例化
        if ( instance == null ) {
            instance = new Singleton();
        }
        return instance;
    }
}

​ 懒汉式就是在创建对象实例前先判断是否已经创建,但是由于对象实例的创建并不是一个原子过程,所以会出现线程安全问题,可以在方法上添加synchronized解决,当然会牺牲一定的性能。基于以上原因,不推荐使用懒汉式的方式实现单例模式。

​ 如何证明对象实例的创建不是一个原子操作?字节码指令可以从侧面证明。

// Java源码
public class Test {
    public Test getTest() {
        return new Test();
    }
}

红框1的位置有三条字节码指令,这还只是字节码的层面,再往低层还会有更多的步骤,所以很明显对象实例的创建不是一个原子操作

2.3 双重检查锁

static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    // 2.volatile保证多线程下的可见性
    private static volatile Singleton instance;
    // 3.向外部暴露一个静态的公共方法
    public static Singleton getInstance() {
        // 3.非空判断
        if ( instance == null ) {
            // 4.同步代码块
            synchronized (Singleton.class) {
                // 5.再次非空判断(保证多线程下的单例)
                if ( instance == null ) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

双重检查锁是最复杂的一种单例模式实现方式,我把它拆分成三个问题:

① 为什么synchronized不加到方法上?

​ 如果添加到方法上,两次非空判断就没有必要了,一次就够了,就转化成了懒汉式(线程安全),这种方式效率不高,因为每次调用都需要获取锁和释放锁。

② 为什么要做两次非空判断?

​ 之前也提到过:对象实例的创建不是一个原子操作。线程安全问题也是出在这一过程中的,解决方案就是添加synchronized关键字,但是添加到方法上效率又太低了;

​ 既然问题是出现在对象实例创建的过程中,那么只对这一段代码进行同步操作(加锁对象就是当前的Class对象,因为对象实例只有一个);

​ 第一层的非空判断是为了如果对象实例已经创建完成了,就不需要再次进入同步代码块了,直接返回创建好的对象实例即可。

③ 为什么要加volatile

根据对象实例创建的字节码指令可以看出对象实例的创建大致分为三步:

​ ① 在堆内存中分配对象内存

​ ② 调用<init>方法,执行对象实例的初始化

​ ③ 将对象引用赋给静态变量

大家应该对JMM模型happens-before有所了解,简单来说JMM模型是对编译器和处理器的约束,happens-before是对开发者的约束。

编译器和处理器在实际运行时,为了执行效率可能会对指令进行重排序的操作,虽然单线程中不会影响执行结果,但是如果是多线程就会出现问题。

像对象实例创建过程的三条指令中②③就有可能会被优化为③②,但是①一定会先执行,因为②③依赖于①,此时执行顺序为①③②,其他线程就会获取到一个未初始化的对象,导致执行出错。

volatile关键字的语义包含两个:

​ ① 保证可见性

​ ② 禁止指令重排序(所以添加volatile后,执行顺序就是①②③了)

​ JDK源码举例:

​ 该类是位于java.lang包下的System类,经典的双重检查锁实现方式。

2.4 静态内部类

static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    // 2.静态内部类,Singleton类加载的时候不会加载内部类,只有用到内部类时才回去加载内部类(保证懒加载)
    private static class SingletonInstance {
        private static final Singleton instance = new Singleton();
    }
    // 3.向外部暴露一个静态的公共方法,此时会装载SingletonInstance,类装载时是线程安全的(保证线程安全)
    public static Singleton getInstance() {
        return SingletonInstance.instance;
    }
}

​ 这是一种很巧妙的方式,相对于饿汉式来说,不需要在类的初始化阶段就创建对象实例,只有在需要(即调用getInstance()方法)的时候才会进行对象实例创建,线程安全也由JVM保证。

​ JDK源码举例:

​ 上图是java.lang.Short源码中的内部类,将常用的整数保存到缓存池当中;下图是访问缓存池中的整数。类似的还有java.lang.Integerjava.lang.Long等包装类。

2.5 枚举

// enum实际上是extends抽象类java.lang.Enum
enum Singleton {
    instance
}

字节码反编译看下:

enum关键字修饰的类实际上继承了java.lang.Enum<E extends Enum<E>

枚举类中声明的实例实际上是public static final修饰的常量

上图为枚举类中<clinit>方法的字节码指令,也就是类的初始化阶段需要执行的逻辑(即将静态变量,静态代码块整合到一块)。

红框1:创建Singleton枚举类对象实例,实际上调用了java.lang.Enum类的构造器(即<init>方法),构造器参数是("INSTANCE", 0),可以通过ldc #7iconst_0看出来;对象实例创建完成后,将实例引用赋给INSTANCE常量。

红框2:将上一步创建的对象实例引用保存到枚举类内部数组$VALUES中,外部可以通过values()方法返回所有的枚举对象引用;数组的创建是在iconst_1anewarray,意思是创建一个长度为1的引用类型数组

posted @ 2022-07-17 21:22  飒沓流星  阅读(457)  评论(0编辑  收藏  举报