单例模式(静态内部类、DCL两种方式以及可能出现的问题)

595 阅读3分钟

静态内部类

public class SingleInstanceTest {

    //私有化构造函数
    private SingleInstanceTest() {
    }

    //静态内部类
    private static class InstanceHolder{
        public static SingleInstanceTest INSTANCE = new SingleInstanceTest();
    }

    //外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存
    public static SingleInstanceTest getInstanceStatic(){
        //在这里初始化
        return InstanceHolder.INSTANCE;
    }
}

JVM在初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

虚拟机会保证一个类的clinit()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕。如果在一个类的clinit()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行clinit()方法后,其他线程唤醒之后不会再次进入clinit()方法。同一个加载器下,一个类型只会初始化一次。

基于这个特性,可以使用这个方式实现线程安全的延迟初始化方案,这种方式的优点就是在外部类加载时并需要立即加载内部类,内部类不加载,则不会初始化INSTANCE,也就不会占用内存。同时这种初始化方案是线程安全的

双重检查锁定

public class SingleInstanceTest {

    private SingleInstanceTest() {
    }

    //volatile关键字
    public static volatile SingleInstanceTest singleInstanceTest;

    public static SingleInstanceTest getInstance() {

        //第一次检查
        if (singleInstanceTest == null) {
            synchronized (SingleInstanceTest.class) {
                //第二次检查
                if (singleInstanceTest == null) {

                    //分配内存空间 memory = allocate()
                    //初始化对象   ctorInstance(memory)
                    //将singleInstanceTest指向刚分配的内存地址
                    singleInstanceTest = new SingleInstanceTest();
                }
            }
        }
        return singleInstanceTest;
    }
}

这种方式有几个点需要注意一下:

  • 1.第一次判空的目的:如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作,因此可以大幅降低synchronized带来的性能开销
  • 2.第二次判空的目的:想象一下这种情况,线程1、2同时判断第一次为空,在加锁的地方的阻塞了,如果没有第二次判空,那么线程1执行完毕后线程2就会再次执行,这样就初始化了两次。两次判空后,DCL就安全多了,一般不会存在问题。但当并发量特别大的时候,还是会存在风险的。也就是volatile改发挥作用了。
  • 3.volatile的作用:singleInstanceTest = new SingleInstanceTest();这行代码可以分解为
  • 1.分配内存空间 memory = allocate()
  • 2.初始化对象 ctorInstance(memory)
  • 3.将singleInstanceTest指向刚分配的内存地址 这三个步骤,第二步和第三步可能会被重排序,这个重排序在没有改变单线程程序执行结果的情况下可以提高程序的执行效率,但是在多线程情况下,如果先执行了第三步,还没来得及执行第二步,另一个并发程序B在判断空的时候,会认定instance不为null,导致B访问到一个未初始化的对象。 volatile的作用就是可以保证程序执行的有序性。避免上述情况的发生。