前言
此前写过设计模式的文章:《单例模式》,谈过单例模式,但对背后的底层知识阐述的还不够到位,比如下面几个问题剖析的不够仔细:
-
静态内部类的实现方案,为何是线程安全的?
-
DCL优化(双重校验模式),为何会线程不安全?又该如何优化?
-
枚举类为何天生特殊,一定线程安全?
概念
创建型模式是用来创建对象的模式,抽象了实例化的过程,帮助一个系统独立于其他关联对象的创建、组合和表示方式。
单例模式 的目的:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
go
单例模式也是创建型的设计模式之一,本文是设计模式系列(共24节)的第2篇文章。设计模式是基于六大设计原则进行的经验总结:《第一节:设计模式的六大原则》创建型设计模式共5种:
单例模式(Singleton Pattern):一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式(Singleton Pattern)可以说是整个设计中最简单的模式之一,且这种模式即使在没有看设计模式相关资料也经常在编码开发中。因为在编程开发中经常会遇到这样一种场景,那就是需要保证一个类只有一个实例哪怕多线程同时访问,并需要提供一个全局访问此实例的点。
综上以及我们平常的开发中,可以总结一条经验,单例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升提升整体的代码的性能。
普通模式(非线程安全)
cs
public class SingletonClassV1 {
private static SingletonClassV1 INSTANCE = null;
private SingletonClassV1() {}
/**
* 非线程安全
* @return
*/
public static SingletonClassV1 getInstance(){
if (INSTANCE == null) {
INSTANCE = new SingletonClassV1();
}
return INSTANCE;
}
}
- 不足:非线程安全,并发情况下,可能创建了多个实例
饿汉模式(线程安全)
cs
public class SingletonClassV2 {
//类初始化时,就已经创建对象,因此线程安全
private static SingletonClassV2 INSTANCE = new SingletonClassV2();
private SingletonClassV2() {}
/**
* 线程安全
* @return
*/
public static SingletonClassV2 getInstance(){
if (INSTANCE == null) {
INSTANCE = new SingletonClassV2();
}
return INSTANCE;
}
}
-
好处:类在加载时就直接初始化了实例。即使没用到,也会实例化,因此,它也是线程安全的单例模式。
-
不足:导致系统加载时间变长,同时也提前占用资源(有没有按需使用资源的场景呢?)
懒汉模式(加锁&线程安全)
java
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优化(双重校验模式)
java
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每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。
静态内部类(线程安全)
cs
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模式里自己斟酌。
枚举类(线程安全)
cs
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实例,它们在类加载时被创建并初始化。
模式应用:日志工具类(静态内部类)
企业应用按规范去打印日志,只要一个单例工具类完成即可。
java
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();
}
}