阅读 218

优雅的实现单例

单例模式属于创建型设计模式,一个类在虚拟机中只有一份实例。实现单例模式的核心思想在于构造函数私有化,主要实现方式分为两种:懒汉式和饿汉式。

单例模式属于创建型设计模式,一个类在虚拟机中只有一份实例。实现单例模式的核心思想在于构造函数私有化,主要实现方式分为两种:懒汉式和饿汉式。

饿汉式-线程安全

//饿汉式单例
public class HungryTypeSingleByFinal {
    private static final HungryTypeSingleByFinal hungryTypeSingle = new HungryTypeSingleByFinal();

    public static HungryTypeSingleByFinal getInstance(){
        return hungryTypeSingle;
    }

    private HungryTypeSingleByFinal(){}
}
复制代码

饿汉式-线程安全

与上面一样,都是在类加载的时候就完成了初始化

//饿汉式单例
public class HungryTypeSingleByStaticBlock {
    private static HungryTypeSingleByStaticBlock hungryTypeSingleByStaticBlock;
    static {
        hungryTypeSingleByStaticBlock = new HungryTypeSingleByStaticBlock();
    }
    private HungryTypeSingleByStaticBlock(){}

    public static HungryTypeSingleByStaticBlock getInstance(){
        return hungryTypeSingleByStaticBlock;
    }
}
复制代码

懒汉式-线程不安全

//懒汉式单例(多线程下不安全)
public class LazyTypeSingleNoSafe {
    private static LazyTypeSingleNoSafe lazyTypeSingleNoSafe;

    public static LazyTypeSingleNoSafe getInstance(){
        if(lazyTypeSingleNoSafe == null){
            lazyTypeSingleNoSafe = new LazyTypeSingleNoSafe();
        }
        return lazyTypeSingleNoSafe;
    }

    private LazyTypeSingleNoSafe(){}
}
复制代码

懒汉式-方法加锁-线程安全

//懒汉式单例 + 获取对象加锁
public class LazyTypeSingleSafe {
    private static LazyTypeSingleSafe lazyTypeSingleSafe;

    public static synchronized LazyTypeSingleSafe getInstance(){
        if(lazyTypeSingleSafe == null){
            lazyTypeSingleSafe = new LazyTypeSingleSafe();
        }
        return lazyTypeSingleSafe;
    }

    private LazyTypeSingleSafe(){}
}
复制代码

懒汉式-双重检查-线程安全

//懒汉式单例 + 双重锁检查
public class LazyTypeSingleSafeDoubleCheck {
    private static LazyTypeSingleSafeDoubleCheck lazyTypeSingleSafeDoubleCheck;
    public static LazyTypeSingleSafeDoubleCheck getInstance(){
        if(lazyTypeSingleSafeDoubleCheck == null){
            synchronized (LazyTypeSingleSafeDoubleCheck.class){
                if(lazyTypeSingleSafeDoubleCheck == null){
                    lazyTypeSingleSafeDoubleCheck = new LazyTypeSingleSafeDoubleCheck();
                }
            }
        }
        return lazyTypeSingleSafeDoubleCheck;
    }
    private LazyTypeSingleSafeDoubleCheck(){}
}
复制代码

这种做法相对于上面的做法的好处就是,如果已经实例化了则直接返回对象,而不是像上面那样每次都进入同步方法,双重检查只是在对象未初始化的时候加锁,一旦对象已经初始化则后面的线程无需加锁直接获取到了单例对象,无疑减小了开销。

表面看起来线程安全,逻辑也没问题,实则有漏洞,后面会讲

懒汉式-静态内部类-线程安全(推荐)

//懒汉式单例 静态内部类实现(推荐)
public class LazyTypeSingleSafeInnerClass {
    private static class LazyTypeSingleSafeInnerClassHolder{
        private static LazyTypeSingleSafeInnerClass singleSafe = new LazyTypeSingleSafeInnerClass();
    }
    public static LazyTypeSingleSafeInnerClass getInstance(){
        return LazyTypeSingleSafeInnerClassHolder.singleSafe;
    }
    private LazyTypeSingleSafeInnerClass(){}
}
复制代码

这种方式是比较推荐的方式,从外部无法访问静态内部类 LazyTypeSingleSafeInnerClassHolder,只有当调用 LazyTypeSingleSafeInnerClass.getInstance 方法的时候,才能得到单例对象 singleSafe。 这里要注意的是 singleSafe 对象初始化的时机并不是在单例类 LazyTypeSingleSafeInnerClass 被加载的时候,而是在调用 getInstance 方法,使得静态内部类 LazyTypeSingleSafeInnerClassHolder 被加载的时候。因此这种实现方式是利用 classloader 的加载机制来实现懒加载,并保证构建单例的线程安全。

无法破解的单例模式

上述单例模式都可以通过反射的方式构造出新的对象,毕竟反射大法香呀:

public static void testLazyTypeSingleSafeInnerClass() throws Exception {
    System.out.println(LazyTypeSingleSafeInnerClass.getInstance() == LazyTypeSingleSafeInnerClass.getInstance());
    //true
    Class<LazyTypeSingleSafeInnerClass> innerClassClass = LazyTypeSingleSafeInnerClass.class;
    Constructor<LazyTypeSingleSafeInnerClass> constructor = innerClassClass.getDeclaredConstructor(null);
    constructor.setAccessible(true);

    System.out.println(constructor.newInstance(null) == constructor.newInstance(null));
    //false
}

public static void threadEnvTest() throws InterruptedException {
    Set<LazyTypeSingleNoSafe> typeSingleNoSafeList = new HashSet<>();

    AtomicInteger atomicInteger = new AtomicInteger(0);

    while(atomicInteger.getAndSet(atomicInteger.intValue() + 1) < 50){
        new Thread(()->{
            LazyTypeSingleNoSafe instance = LazyTypeSingleNoSafe.getInstance();
            typeSingleNoSafeList.add(instance);
        }).start();
    }

    for(LazyTypeSingleNoSafe lazyTypeSingleNoSafe: typeSingleNoSafeList){
        System.out.println(lazyTypeSingleNoSafe);
    }
    //single.LazyTypeSingleNoSafe@6e5802d8
    //single.LazyTypeSingleNoSafe@6706a70b
    //single.LazyTypeSingleNoSafe@51a22f1d
}
复制代码

通过上述例子我们也看到了,在反射眼里,一切都是弟弟,所以我们根本不可能造出真正的单例,但是我们却可以通过枚举这个特性来实现绝对的单例模式和多例模式!

//绝对的单例模式(之前的通过反射都可以破解)
public enum AbsoluteSingleSafe {
    ONLY_ONE_SINGLE
}
复制代码

我们破解一下枚举试试:

public static void testAbsoluteSingleSafe() throws Exception {
    System.out.println(AbsoluteSingleSafe.ONLY_ONE_SINGLE == AbsoluteSingleSafe.ONLY_ONE_SINGLE);
    //true

    Class<AbsoluteSingleSafe> absoluteSingleSafeClass = AbsoluteSingleSafe.class;
    Constructor<AbsoluteSingleSafe> constructor = absoluteSingleSafeClass.getDeclaredConstructor(null);
    constructor.setAccessible(true);
    System.out.println(constructor.newInstance(null) == constructor.newInstance(null));
    //Exception in thread "main" java.lang.NoSuchMethodException: single.AbsoluteSingleSafe.<init>()
}
复制代码

DoubleCheck的隐患

我们回顾一下 DoubleCheck 的代码:

public class LazyTypeSingleSafeDoubleCheck {
    private static LazyTypeSingleSafeDoubleCheck lazyTypeSingleSafeDoubleCheck;
    public static LazyTypeSingleSafeDoubleCheck getInstance(){
        if(lazyTypeSingleSafeDoubleCheck == null){
            synchronized (LazyTypeSingleSafeDoubleCheck.class){
                if(lazyTypeSingleSafeDoubleCheck == null){
                    lazyTypeSingleSafeDoubleCheck = new LazyTypeSingleSafeDoubleCheck();
                }
            }
        }
        return lazyTypeSingleSafeDoubleCheck;
    }
    private LazyTypeSingleSafeDoubleCheck(){}
}
复制代码

问题出在哪里呢?

我们可以假设这样的情况,当两个线程一先一后访问 getInstance 方法的时候,当 A 线程正在构建对象,B 线程刚刚进入方法:

这种情况表面看似没什么问题,要么 Instance 还没被线程 A 构建,线程 B 执行 if(lazyTypeSingleSafeDoubleCheck== null) 的时候得到 true;要么 Instance 已经被线程 A 构建完成,线程 B 执行 if(lazyTypeSingleSafeDoubleCheck== null) 的时候得到 false。真是如此吗?答案是否定的。这里涉及到了 JVM 编译器的指令重排。

一句简单的lazyTypeSingleSafeDoubleCheck = new LazyTypeSingleSafeDoubleCheck(); 会被编译器编译成如下 JVM 指令 :

memory = allocate(); //1:分配对象的内存空间

ctorInstance(memory); //2:初始化对象

instance =memory; //3:设置 instance 指向刚分配的内存地址

但是这些指令顺序并非一成不变,有可能会经过 JVM 和 CPU 的优化,指令重排成下面的顺序:

memory =allocate(); //1:分配对象的内存空间

instance =memory; //3:设置 instance 指向刚分配的内存地址

ctorInstance(memory); //2:初始化对象

当线程 A 执行完 1 和 3 时,instance 对象还未完成初始化,但已经不再指向 null。此时如果线程 B 抢占到 CPU 资源,执行 if(instance == null) 的结果会是 false,从而返回一个没有初始化完成的 instance 对象。如下图所示:

由于线程 A 还未完成初始化工作,但是线程 B 检测到对象已经不为空,于是最终返回的是空对象!!那么应该如何避免呢?其实只需要在 instance 对象前面增加一个修饰符 volatile 就好了,关于可以看《重新认识 volatile》 这篇文章,里面讲述的比较详细,在此不再赘述。所以完整的双重检查的代码是:

//懒汉式单例 + 双重锁检查
public class LazyTypeSingleSafeDoubleCheck {
    private volatile static LazyTypeSingleSafeDoubleCheck lazyTypeSingleSafeDoubleCheck;
    public static LazyTypeSingleSafeDoubleCheck getInstance(){
        if(lazyTypeSingleSafeDoubleCheck == null){
            synchronized (LazyTypeSingleSafeDoubleCheck.class){
                if(lazyTypeSingleSafeDoubleCheck == null){
                    lazyTypeSingleSafeDoubleCheck = new LazyTypeSingleSafeDoubleCheck();
                }
            }
        }
        return lazyTypeSingleSafeDoubleCheck;
    }
    private LazyTypeSingleSafeDoubleCheck(){}
}
复制代码

单例模式的总结

所以如果想要实现线程安全的单例模式,可以使用饿汉式、DoubleCheck(加 volatile 的版本),静态内部类,枚举等方式;想要使用懒加载策略就不能使用枚举了,只能 DoubleCheck(加 volatile 的版本),静态内部类;如果想实现反射也无法破解的单例那么只能用枚举了,但是一般情况下不会去刻意排斥反射。所以比较推荐的方案还是静态内部类,简单实用而且线程安全。

文章分类
前端