SINGLETON

  • 核心作用:
    • 保证一个类只有一个实例,并且提供一个访问该实例的全局访问点
  • 常见场景:
    • Windows的任务管理器
    • Spring中的bean
  • PROS:
    • 只生成一个实例,减少系统开销
    • 设置全局的访问点,优化共享资源访问
  • 常见的五种单例模式:
    • 饿汉模式、懒汉模式、DCL懒汉模式、静态内部类模式、枚举单例

一、饿汉模式

public class Demo1 {
    // 私有化构造器
    private Demo1() {}
    // static : 只会生成一份实例,不存在并发问题
    // 类初始化的时候立即加载该对象
    private static Demo1 instance = new Demo1();
    // 提供获取该对象的 方法,没有synchronized(考虑并发问题要加synchronized),效率高
    public static Demo1 getInstance() {
        return instance;
    }
}

二、懒汉模式

public class Demo2 {
    private Demo2() {}
    // 类初始化时,不立即加载对象
    private static Demo2 instance;
    // 由于线程不安全,加上synchronized,使它成为一个同步方法
    public static synchronized Demo2 getInstance() {
        if (instance == null) {
            // 线程 1 在这里等待
            instance = new Demo2();
        }
        return instance;
    }
}

为什么懒汉模式线程不安全?

  • 假设有两个线程,线程1、线程2 ,当线程 1 在上面所示位置等待时,cpu突然停止使用线程 1,转而去调用线程2。线程2执行了instance == null后去new对象,当轮到线程1使用时,仍认为instance == null,就会再一次去new对象。
  • 这种单例模式在并发数越高的情况下越容易产生问题

由于这种模式效率低下,于是出现了双重检测机制

三、双重检测机制(DCL)懒汉式

public class Demo3 {
    private Demo3() {}
    // volatile:当一个变量对它修改时,另一个变量对它修改时的缓存就失效了
    // 加了volatile能避免指令的重排
    private volatile static Demo3 instance;
    public static Demo3 getInstance() {
        if (instance == null) {
            // 将锁范围缩小,提高性能
            synchronized (Demo3.class) {
                if (instance == null) {
                    // 其实锁住这里面,有一次判断为空就够了,
                    // 外面那个判断为空主要为了提高性能
                    instance = new Demo3();
                }
            }
        }
        return instance;
    }
}

由于jvm存在乱序执行功能,DCL也会出现线程不安全的情况(未加volatile的情况),分析如下:

instance = new Demo3();

这个步骤,在jvm里分三次执行:

  1. 在堆内存开辟空间
  2. 在堆内存实例化Demo3里面的各个参数
  3. 把对象指向堆内存空间

由于jvm存在乱序执行功能,所以可能在2没执行时就执行3,如果此时切换到其它线程,由于执行了3,instance已经非空了,会被直接拿来用,这样的话就会出现异常。这就是著名的DLC失效问题。

不过在jdk1.5之后,官方也发现了这个问题,在jdk1.6后,使用了volatile,定义为private volatile static Demo3 instance;就能解决DLC失效问题。volatile确保instance每次都在主内存中读取,这样会牺牲一点效率,但也无伤大雅。

四、静态内部类模式

public class Demo4 {
    private Demo4() {}
    // 静态内部类
    private static class Inner {
        private static final Demo4 instance = new Demo4();
    }
    public static Demo4 getInstance() {
        return Inner.instance;
    }
}

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不会去初始化instance,故而不占内存。只有当getInstance()方法第一次被调用时,才会去初始化instance


那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。

类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。

  1. 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
  3. 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
  5. 当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。


那么instance在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:

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

故而,可以看出instance在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。


缺点:

  • 那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。

用反射破坏

class Demo4Test {
    public static void main(String[] args) throws Exception {
        Demo4 instance1 = Demo4.getInstance();
        Constructor<Demo4> demo4Constructor = Demo4.class.getDeclaredConstructor(null);
        demo4Constructor.setAccessible(true);
        Demo4 instance2 = demo4Constructor.newInstance();
        System.out.println(instance1 == instance2);      //false
    }
}

五、枚举单例

public enum Demo5 {
    INSTANCE;           //自动就是单例
    public Demo5 getInstance() {
        return INSTANCE;
    }
}
class test {
    public static void main(String[] args) {
        Demo5 instance1 = Demo5.INSTANCE;
        Demo5 instance2 = Demo5.INSTANCE;
        System.out.println(instance1 == instance2);     //true
    }
}

六、总结:

  • 饿汉式(线程安全,调用效率高,不能延时加载)
  • 懒汉式(线程安全,调用效率不高,可以延时加载)
  • 双重检测机制(DCL)懒汉式(线程安全,调用效率较高,可以延时加载)
  • 饿汉改进:静态内部类(线程安全,调用效率高,可以延时加载)
  • 枚举单例(线程安全,调用效率高,不能延时加载)

前面四种都可以被反射机制破坏