前言
此前写过设计模式的文章:《单例模式》,谈过单例模式,但对背后的底层知识阐述的还不够到位,比如下面几个问题剖析的不够仔细:
静态内部类的实现方案,为何是线程安全的?
DCL优化(双重校验模式),为何会线程不安全?又该如何优化?
枚举类为何天生特殊,一定线程安全?
概念
创建型模式是用来创建对象的模式,抽象了实例化的过程,帮助一个系统独立于其他关联对象的创建、组合和表示方式。
单例模式的目的:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式也是创建型的设计模式之一,本文是设计模式系列(共24节)的第2篇文章。设计模式是基于六大设计原则进行的经验总结:《第一节:设计模式的六大原则》创建型设计模式共5种:
单例模式(Singleton Pattern):一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式(Singleton Pattern)可以说是整个设计中最简单的模式之一,且这种模式即使在没有看设计模式相关资料也经常在编码开发中。因为在编程开发中经常会遇到这样一种场景,那就是需要保证一个类只有一个实例哪怕多线程同时访问,并需要提供一个全局访问此实例的点。
综上以及我们平常的开发中,可以总结一条经验,单例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升提升整体的代码的性能。
普通模式(非线程安全)
public class SingletonClassV1 {private static SingletonClassV1 INSTANCE = null;private SingletonClassV1() {}/*** 非线程安全* @return*/public static SingletonClassV1 getInstance(){if (INSTANCE == null) {INSTANCE = new SingletonClassV1();}return INSTANCE;}}
不足:非线程安全,并发情况下,可能创建了多个实例
饿汉模式(线程安全)
public class SingletonClassV2 {//类初始化时,就已经创建对象,因此线程安全private static SingletonClassV2 INSTANCE = new SingletonClassV2();private SingletonClassV2() {}/*** 线程安全* @return*/public static SingletonClassV2 getInstance(){if (INSTANCE == null) {INSTANCE = new SingletonClassV2();}return INSTANCE;}}
好处:类在加载时就直接初始化了实例。即使没用到,也会实例化,因此,它也是线程安全的单例模式。
不足:导致系统加载时间变长,同时也提前占用资源(有没有按需使用资源的场景呢?)
懒汉模式(加锁&线程安全)
public class SingletonClassV3 {//类初始化时,就已经创建对象,因此线程安全private static SingletonClassV3 INSTANCE = null;private SingletonClassV3() {}/*** 线程安全* @return*/public static synchronized SingletonClassV3 getInstance(){if (INSTANCE == null) {INSTANCE = new SingletonClassV3();}return INSTANCE;}}
好处:懒加载了,也线程安全了
不足:将方法强行锁了,可能导致性能问题(有没有性能更好一点的办法呢?)
懒汉模式-DCL优化(双重校验模式)
public class SingletonClassV4 {// 加了volatile,就能解决【1】的问题private static volatile SingletonClassV4 INSTANCE = null;private SingletonClassV4() {}/*** 【1】JVM的指令重排序,可能导致并发下的重复创建* @return*/public static SingletonClassV4 getInstance(){// 第一次检测if (INSTANCE == null) {synchronized (SingletonClassV4.class) {// 第二次检测if (INSTANCE == null) {INSTANCE = new SingletonClassV4();}return INSTANCE;}}return INSTANCE;}}
好处:懒加载了,只锁一部分代码段
不足:可能因为JVM存在乱序执行功能,DCL也会出现线程不安全的情况
不过在JDK1.5之后,官方也发现了这个问题,故而具体化了volatile,
即在JDK1.6及以后,只要定义为 private volatile static SingleTon INSTANCE = null;就可解决DCL失效问题。volatile确保INSTANCE每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。
静态内部类(线程安全)
package com.bryant.singleton;public class SingletonClassV5 {private static class SingleTonHoler{private static SingletonClassV5 INSTANCE = new SingletonClassV5();}/*** 私有化构造器*/private SingletonClassV5() {}/*** 获取单例方法,getInstance()获取单例的方法,不会触发多次new操作,所以只会返回同一个对象* @return*/public static SingletonClassV5 getInstance() {return SingleTonHoler.INSTANCE;}public static void main(String[] args) {SingletonClassV5 instance = SingletonClassV5.getInstance();System.out.println(instance.hashCode());}}
好处:用到了静态内部类的懒加载特性,做到了线程安全
静态内部类的特殊性
JVM主动加载类
JVM有5个主动引用而类加载的场景,分别是:
遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类
当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
静态内部类-被动加载
而静态内部类并不在5种情况之内,所以静态内部类,是绝对是用到了才会加载的资源,所以不会触发提前加载。
因此,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
静态内部类-线程安全
当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。
故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。可以参考:Java虚拟机:浅谈静态代码块和方法
不足
是不是可以说静态内部类单例就是最完美的单例模式了呢?
其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。
枚举类(线程安全)
public class SingletonClassV6 {enum SingletonEnum {INSTANCE;//懒加载,创建一个枚举对象,该对象天生为单例private SingletonClassV6 singleton;//私有化枚举的构造函数(强调不可外部实例化)private SingletonEnum() {singleton = new SingletonClassV6();}public static SingletonClassV6 getEnumInstance(){return INSTANCE.singleton;}}public static SingletonClassV6 getInstance(){return SingletonEnum.getEnumInstance();}}
好处:实现了懒加载
枚举在java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例。
Java编译器会将枚举类,转换为一个继承自java.lang.Enum的类。这意味着枚举本质上是一个特殊的类。
枚举常量是该枚举类的静态final实例,它们在类加载时被创建并初始化。
模式应用:日志工具类(静态内部类)
企业应用按规范去打印日志,只要一个单例工具类完成即可。
public class BusinessLogUtil {private static Logger logger = LoggerFactory.getLogger(BusinessLogUtil.class);private BusinessLogUtil() {}public static final BusinessLogUtil getInstance() {return SingletonHolder.INSTANCE;}private static class SingletonHolder {private static final BusinessLogUtil INSTANCE = new BusinessLogUtil();}
}