彻底搞懂单例模式如何安全的实现

Java基础

浏览数:294

2019-3-25

单例设计模式,意味着整个系统中只能存在一个实例,比方说像日志对象这种。我们常说的有饿汉式和懒汉式这两种模式来创建单例对象,今天就拓展一下思维,多看几种。

 

首先我们若是想一个类只有一个对象,那肯定先要私有化构造器,断了在其它的类中使用构造器创建实例的念头。其它的类中不能创建,我们就只能在类中自己创建一个私有实例,另外还要提供一个共有的方法使其它对象获取到实例。所以,第一版出现了。

 

1 【饿汉式 V1】

在类加载的时候就创建实例

 

@ThreadSafe
public class SingletonExample2 {
    // 私有化构造器
    private SingletonExample2(){}
    // 提供一个实例
    private static SingletonExample2 instance = new SingletonExample2();
    // 提供共有的方法返回实例
    public static SingletonExample2 getInstance(){
        return instance;
    }
}

 

不要忘了在多线程环境中还有关注线程是否安全,我这里都会打上注解,@ThreadSafe 表示线程安全,@NotThreadSafe 表示线程不安全。

 

上面这种方式就是比较简单的,也是最容易想到的方式,就有一个缺点,若是不使用这个对象,那就有点浪费资源了,这个对象不一定会被使用,但是我们已经创建好了。

 

2 【饿汉式 V2】

这种方式是借助于 “静态代码块只会被加载一次” 来实现单例的创建,很简单,也很好理解,问题和饿汉式一样,不一定就会使用到这个对象,所以可能会出现浪费资源的情况。

 

@ThreadSafe
public class SingletonExample6 {
    // 私有化构造器
    private SingletonExample6(){}

    private static SingletonExample6 instance = null;

    static {
        instance = new SingletonExample6();
    }
    // 提供共有的方法返回实例
    public static SingletonExample6 getInstance(){
        return instance;
    }
}

 

3 【懒汉式 V1】

在对象使用的时候才创建实例

 

@NotThreadSafe
public class SingletonExample1 {
    // 私有化构造器
    private SingletonExample1(){}
    // 提供一个实例
    private static SingletonExample1 instance = null;
    // 提供共有的方法返回实例
    public static SingletonExample1 getInstance(){
        if(instance == null){
            return new SingletonExample1();
        }
        return instance;
    }
}

 

这种方式在单线程的时候是没有问题的,但是在多线程时就会出现问题,假如线程 A 进入 if 之后暂停执行,此时又来一个线程 B 还是可以进入 if 并返回一个实例,此时 A 再次获得执行时,返回的是另一个实例了。

 

4 【懒汉式 V2】

在共有方法上添加 synchronized 关键字,同步该方法。可行,但是不推荐使用,因为 synchronized 修饰方法之后,在同一时刻只能有一个线程执行该方法,一旦有线程获得方法,其它线程需要等待,这样会浪费大量时间,系统运行效率降低。

 

@ThreadSafe
@NotRecommend
public class SingletonExample3 {
    // 私有化构造器
    private SingletonExample3(){}
    // 提供一个实例
    private static SingletonExample3 instance = null;
    // 提供共有的方法返回实例
    public static synchronized SingletonExample3 getInstance(){
        if(instance == null){
            return new SingletonExample3();
        }
        return instance;
    }
}

 

5 【懒汉式 V3】

这种方式使用双重检测 + 防止指令重排的方式来保证线程安全,首先需要注意的是在 getInstance 方法中,我们需要双层检测并使用同步代码块将创建对象的过程同步起来。

 

@NotThreadSafe
public class SingletonExample4 {
    // 私有化构造器
    private SingletonExample4(){}
    // 提供一个实例
    private static SingletonExample4 instance = null;
    // 提供共有的方法返回实例
    public static SingletonExample4 getInstance(){
        // 线程 B 判断,发现 instance 不为空,直接返回,而实际上 instance 还没有初始化。
        if(instance == null){  // 双重检测机制
            synchronized (SingletonExample4.class) {  // 同步锁
                if(instance == null){
                    // 线程 A 执行到重排后的指令 3 ,此时 instance 已经有地址值了。但是没有初始化
                    return new SingletonExample4(); // 这里是重点!!
                }
            }
        }
        return instance;
    }
}

 

因为在 new SingletonExample4() 的过程中,并不是一个原子操作,是可以进一步拆分为:

 

1、分配对象内存空间

memory = allocate()

2、初始化对象

initInstance()

3、设置 instance 指向刚分配的内存

instance = memory

 

在多线程的情况下,上面 3 个指令会存在指令重排序的情况。【JVM 和 CPU 指令优化】重排后的结果可能为:

 

memory = allocate()

instance = memory

initInstance()

 

此时可能会存在线程 A 在内层 if 执行到指令重排后的第 3 步,但并未初始化,只是存在了地址值,线程 B 在外层 if 判断时,会直接 return 实例,而这个实例是一个只有地址值而没有被初始化的实例。

 

为了防止指令重排带来的问题呢,我们就可以使用 volatile 关键字防止指令重排。这样就是线程安全的了。只需在上一版的基础上使用 volatile 修饰 instance 实例即可。

 

volatile 的语义就是添加内存屏障和防止指令重排,这在前面已经分析过了。


    private static volatile SingletonExample4 instance = null;

 

6 【使用枚举类实现单例模式】

这是推荐使用的方法,因为它比懒汉式的线程安全更容易保证,比饿汉式的性能高,它只有在调用的时候才实例对象。

 

@ThreadSafe
@Recommend
public class SingletonSpecial {
    private SingletonSpecial(){}

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

    private enum Singleton{
        INSTANCE;
        // public static final Singleton INSTANCE;

        private SingletonSpecial singleton;

        // JVM 来保证这个构造方法只会调用一次
        Singleton(){
            singleton = new SingletonSpecial();
        }

        public SingletonSpecial getInstance(){
            return singleton;
        }
    }
}

 

7 【使用静态内部类】

这种方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会加载 SingletonInstance 类,从而完成 Singleton 的实例化。

 

使用 static final 修饰之后 JVM 就会保证 instance 只会初始化一次且不会改变。

 

@ThreadSafe
@Recommend
public class SingletonExample7 {

    private SingletonExample7(){}

    private static class SingletonInstance{
        private static final SingletonExample7 instance = new SingletonExample7();
    }

    public static SingletonExample7 getInstance(){
        return SingletonInstance.instance;
    }
}

 

总结一下,今天主要说了单例模式的实现,并且在这中间,复习了一下前面说的线程安全的应用。若是对线程安全的原理以及实现有不懂的可以回头看看前面几篇文章。