单例模式共有四种实现方式:

实现方式线程安全懒加载防止反射
静态成员属性xx
DCL 机制x
静态内部类x
枚举x

一、静态成员属性

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

二、DCL 机制

DCL 又称双重检测锁机制
public class Singleton{
    // 注意要加 volatile 防止指令重排序
    private volatile Singleton instance;
    
    private Singleton(){}
    
    public Singleton getInstance(){
        // 两个 if 即为双重检测锁机制
        if(instance == null){
            synchronized(Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

重点:

  • 使用 synchronized 锁住 new Singleton() 代码,防止多个线程创建出多个实例
  • if 判断需要在 synchronized 内部。如果在外部,则多线程环境下无法保证只创建一个实例
  • synchronized 外部的 if 其实可以去除,加在这里主要是为了优化性能。如果不在外部做一个 if 判断,那么每个线程 getInstance 时,不管实例是否已经创建,都要等待锁,性能太差。
  • volatile 防止指令重排序。指令重排序是 cpu 在执行汇编指令时为了优化性能,可能会对指令做一个重新排序。比如 new Singleton() 这个代码,有三条指令:

    • 分配对象的内存空间
    • 初始化对象
    • 将 instance 指向刚分配的内存地址

正常情况这三条指令顺序执行,没有问题。但可能经过 JVM 和 CPU 的优化,顺序会变成下面的样子:

  • 分配对象的内存空间
  • 将 instance 指向刚分配的内存地址
  • 初始化对象

在这种情况下,就会出现问题。比如线程1执行到上面的第二步将 instance 指向刚分配的内存地址,此时线程2在进行 if 判断,发现 instance 不为 null 了,就直接返回 instance,但实际上此时 instance 还是一个未初始化的对象,这是线程2在使用该对象时就会出问题。

DCL 机制也可以像下面这样写,但性能很差,并且也不能成为 DCL 机制了,实际中并不建议这样做,这里仅为了方便理解:

public class Singleton{
    // 注意要加 volatile 防止指令重排序
    private volatile Singleton instance;
    
    private Singleton(){}
    
    public Singleton getInstance(){
        // 去除了外层 if,不影响功能,但影响性能
        synchronized(Singleton.class){
            if(instance == null){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

三、静态内部类

支持懒加载,弥补了静态成员变量方式的缺点
public class Singleton{
    private static class LazyHolder{
        private static final INSTANCE = new Singleton();
    }
    
    private Singleton(){}
    
    public Singleton getInstance(){
        return LazyHolder.INSTANCE;
    }
}

该方式利用 JVM 的类加载机制实现懒加载,并且因为是 static 的,也没有线程安全问题。

类加载机制:静态变量仅在被用到时才进行初始化。比如这里 Singleton 被加载并不会导致 LazyHolder.INSTANCE 被加载,只有在调用 Singleton::getInstance 方法时才会去加载 LazyHolder.INSTANCE 变量,以此实现了懒加载

四、枚举

上面三种方式都无法防止用户使用反射来创建实例,而枚举正好可以枚举此缺点。当用户尝试使用反射来创建枚举类的实例时, JVM 会抛出一个错误。

public enum Singleton{
    INSTANCE
}

这种方式简单,但却不是懒加载的,枚举类被加载的时候,该单例对象就会被加载。

Last modification:August 15th, 2020 at 03:38 pm