其实在学Spring5源码的时候,就已经罗列了各种单例模式。在此,学习过 java.util.concurrent 包后,站在多线程的角度上分析比较经典的懒汉模式的双重锁校验,又称为DCL懒汉式(Double Check Lock)。

1、逐步进阶,基础版本:

private static LazySingleton singleton4;

private static LazySingleton lazy() {
    if (singleton4 == null) {
        singleton4 = new LazySingleton();
    }
    return singleton4;
}

存在问题分析:多线程进行访问时,可能多个线程会同时进入if 的作用域中,那么就会创建多个 LazySingleton() 对象,导致这些线程返回的对象地址不一致。

2、普通加锁版本

直接方法加锁,确实能够解决问题,但是性能十分低下。

private static LazySingleton singleton4;

private synchronized static LazySingleton lazy() {
    if (singleton4 == null) {
        singleton4 = new LazySingleton();
    }
    return singleton4;
}

3、双重校验加锁版本

多线程都可以进入方法和第一个 if 的作用域,锁类的目的是保证只能有一个线程进入当前类后进入第二个if作用域创建实例对象,随后解锁,让其它进入第一个if作用域的阻塞线程在判断一次是否为空。
虽然性能是提升了,但是真的线程安全吗?并发下的指令重排是会出问题的,分析: singleton4 = new LazySingleton() 不是原子性操作,有三个步骤: ①分配内存空间 ②执行构造方法并实例化对象 ③ 分配内存地址,把这个对象指向这个空间。CPU执行时不一定是按照123执行的,如果按照执行顺序是132,第3步先执行的话,还没有完成实例化,就指向这个空间了,此时的 singleton4 不为空,下一个线程进入方法的第一个if判断走false,直接返回这个对象,此时的这个 singleton4 并没有完成实例化!所以就会导致数据不一致的问题。

volatile 有三大特性:1、可见性;2、禁止指令重排;3、不保证原子性。所以使用volatile 关键字修饰即可。

private volatile static LazySingleton singleton4;

private static LazySingleton lazy2() throws InterruptedException {
    if (singleton4 == null) {
        synchronized (LazySingleton.class) {
            if (singleton4 == null) {
                singleton4 = new LazySingleton();
            }
            return singleton4;
        }
    }
    return singleton4;
}

4、单例模式被破坏的情况

破坏单例模式的意思就是:一个单例对象存在多个。

反射、序列化、克隆都会破坏单例模式。

解决方案:枚举单例模式、容器式单例模式(ConcurrentHashMap)、线程隔离式单例模式(ThreadLocal)

尝试破坏枚举单例模式:

private enum Lazy4 {
    INSTANCE;

    public static Lazy4 getInstance() {
        return INSTANCE;
    }
}

public static void main(String[] args) throws Exception {
        Lazy4 instance1 = Lazy4.INSTANCE;
        Constructor<Lazy4> declaredConstructor = Lazy4.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        Lazy4 instance2 = declaredConstructor.newInstance(); 
        System.out.println(instance1); System.out.println(instance2);
    }