第3条 单例模式实现方案

阅读Effective Java第3条"用私有构造器或者枚举类型强化Singleton属性",进而拓展单例模式的实现方案,下文将逐个展开阐述。

1. 单例模式的定义

单例(Singleton)就是一个类必须保证有且仅有一个实例存在。
比如,spring通过xml或@Component等注解配置的bean都是单例的

2. 单例模式的好处

1)只有一个对象,内存开支少、性能好
2)避免对资源的多重占用
3)在系统设置全局访问点,优化和共享资源访问

3. 单例模式的实现思路

单例四大原则:
	1)构造方法私有
	2)以静态方法或枚举返回静态化实例
	3)确保实例只有一个,线程安全
	4)确保反射或反序列化不会重新构建对象

4. 单例模式的实现方案

1)final域的公有静态成员
public class Singleton {
	public static final INSTANCE = new Singleton();
	private Singleton() {}
}

当Singleton类加载时,会遇到new字节码指令,因此会触发类加载的最后一个阶段–初始化阶段,初始化阶段是执行类构造函数()方法,() 方法是由编译器自动收集类中的所有类变量(static)的赋值动作和静态语句块(static{})块中的语句合并产生的,因此public static final INSTANCE = new Singleton();也会被放入到这个方法中。因此,在类加载中便完成对象的创建,又因为虚拟机保证() 方法执行是线程安全的,最终只会创建一个Singleton实例。又因为Singleton不存在公有或受保护的构造方法,所以可以保证Singleton的全局唯一性。

缺点:
a. 不能抵御反射攻击。即借助AccessibleObject.setAccessible()方法,通过反射机制调用私有构造函数重新创建新对象。
b. 不能抵御反序列化攻击。普通类的反序列化是通过反射机制来实现的,后面讲述“破坏单例模式问题”时会展开陈述。违反单例原则4)
c. 资源利用不够灵活。假如当前类很少使用到或根本不被使用,那么该实例的内存占用就是白白浪费资源。
d. 通过Singleton.INSTANCE返回实例,通过违反单例原则2)

2)饿汉式(公有静态工厂方法)

基本原理同1),在类加载过程中完成实例的创建,唯一不同的是此处通过静态工厂方法获取实例。

public class Singleton {
	private static final INSTANCE = new Singleton();
	private Singleton() {}
	public static Singleton getInstance() {
		return INSTANCE;
	}
}

优点:灵活,可读性好,便于维护

缺点:同1)

3)懒汉式

为了改善1) 2)中第三个缺点–资源利用不够灵活,通过延迟加载的思路提出了懒汉式单例方案。

a. 普通懒汉式版本
public class Singleton{
  private static Singleton INSTANCE = null;
  private Singleton(){}
  public static Singleton getInstance(){
  	if(INSTANCE == null){
   		INSTANCE = new Singleton();
  	}
  	return INSTANCE;
}

缺点:有线程安全问题。多线程环境下,若多个线程同时运行到INSTANCE == null这一行且都判断为nul了,那么两个线程会各自创建一个实例,显然违反了单例的原则3)

b. synchronized版本
public class Singleton{
  private static Singleton INSTANCE = null;
  private Singleton(){}
  public synchronized static Singleton getInstance(){
  	if(INSTANCE == null){
   		INSTANCE = new Singleton();
  	}
  	return INSTANCE;
}

给getInstance方法加上同步锁,如果有两个线程(T1、T2)同时执行到这个方法时,会有其中一个线程T1获得同步锁,得以继续执行,而另一个线程T2则需要等待,当第T1执行完毕getInstance之后(完成了null判断、对象创建、获得返回值之后),T2线程才会执行执行。——所以这段代码也就避免了普通懒汉式版本中可能出现因为多线程导致多个实例的情况。

缺点:方法级别的同步锁,会对程序的执行效率有所影响。我们应该遵从同步锁的粒度尽量小的原则,比如代码块级别的同步锁。

c.  双重检测(Double-Check-Lock)版本:
public class Singleton{
  private static Singleton INSTANCE = null; 
  private Singleton(){}
  public static Singleton getInstance(){
  	if(INSTANCE == null){
   		synchronized(Singleton.class){
     		if(INSTANCE == null){ 
        		INSTANCE = new Singleton();
       		} 
     	} 
    } 
    return INSTANCE; 
  }
}

优点:同步锁代码块级别,提高程序执行效率

缺点:DCLINSTANCE = new Singleton()存在指令重排而引起的小概率性问题,就是常说的DCL失效问题。

主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1.给 singleton 分配内存
2.调用 Singleton 的构造函数来初始化成员变量,形成实例
3.将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

上面这段话引自https://www.jianshu.com/p/eb30a388c5fc

d. volatile版本
public class Singleton{
  private static volatile Singleton INSTANCE = null; // volatile是禁止指令重排,jdk6开始支持
  private Singleton(){}
  public static Singleton getInstance(){
  	if(INSTANCE == null){
   		synchronized(Singleton.class){
     		if(INSTANCE == null){ 
        		INSTANCE = new Singleton();
       		} 
     	} 
    } 
    return INSTANCE; 
  }
}

这个版本可以说是终极版本了,增加volatile修饰,禁止指令重排。我们知道c版本中存在的问题就是在线程T1写操作还没执行完,线程T2就执行了读操作,而volatile就保证了对INSTANCE的写操作会有一个内存屏障,保证其在写操作完成之前不会有读取INSTANCE的操作。

4) 静态内部类
public class Singleton{
  private static class SingletonHolder {
  	private static Singleton INSTANCE = new Singleton();
  }
  private Singleton(){}
  public static Singleton getInstance(){
  	return SingletonHolder.INSTANCE;
}

优点:具备线程安全性。
当外部类加载时并不需要立即加载内部类(虚拟机的规范),内部类不被加载也就不会初始化INSTANCE,因此就不会提前占用资源,起到延迟加载的效果(类懒汉式)。即当Singleton第一次被加载时,SingletonHolder不会被立即加载,只有当getInstance方法被第一次调用时,会遇到new字节码指令,虚拟机才会加载内部类SingletonHolder,进而初始化INSTANCE,完成对象的创建。相当于在内部类的类加载过程中完成外部类单例实例的创建,如1)所述天生具备线程安全性。

缺点:无法传参。创建实例时无法传入可变参数,因为内部类创建单例时调用的是无参的构造函数,使用起来不方便。

5)枚举类
public enum Singleton {
	INSTNACE;
	private Singleton() {}
}

编译后的字节码反编译后的Java代码是:

public final class Singleton extends Enum<Singleton>{
	public static final Singleton INSTNACE;
    public static Singleton[] values();
    public static Singleton valueOf(String s);
    static {};
}

枚举单例优点:线程安全、抵御反射攻击、抵御反序列化攻击,绝对防止多次实例化。
其中,枚举最重要的三个特性使得枚举实现单例具备先天优势:

a. 枚举类对象创建线程安全。

枚举类本质上是一个final类,第一次使用它的时候被加载和初始化,且INSTNACE本质上其实是一个static的,初始化时也被放在cinit<>()方法中执行,是线程安全的。

b. 枚举类型对象天生不允许通过反射创建。	

Constructor.newInstance()方法中明确禁止了通过反射创建枚举实例。符合单例原则4)

@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
c. 枚举类型的特殊的反序列化机制。

普通类的反序列化是通过反射调用实现的,而枚举类不是。Java规范中规定,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。因此,枚举类型反序列化不会创建新对象。比如上述Singleton的INSANCE实例反序列化的时候就是通过valueOf(INSANCE)来查找枚举对象,而不是新建

枚举单例缺点:无法继承。因为枚举类是final类且构造函数是私有的

5. 破坏单例模式的问题及解决办法

1)反射会重新创建对象

反射能够调用类的私有构造函数创建新的对象,从而破坏单例模式。

解决办法:私有构造函数增加校验,避免反射。该方法适用于上述除枚举实现单例以外的所有单例实现方案

private static boolean flag = fase;
private Singleton() {
	synchronized(Singleton.class) {
		if (flag) {
			flag = ! flag;	
		}else {
			throw RuntimeException("单例模式被侵犯!");
		}
	}
}
2)反序列化会重新创建对象

普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。

解决办法:必须所有实例域都是瞬时(transient)的,并提供一个readResolve方法。该方法适用于上述除枚举实现单例以外的所有单例实现方案。如下:

private Object readResolve() {
	return INSTANCE;
}

6. 总结

单例模式实现方案有很多种,每种都有利弊,使用时需要根据场景来权衡选择,比如从单例模式的四个原则来看枚举实现单例是最理想的方式,但是它的缺陷在于不能被继承。

7. 参考文献

《深入理解JAVA虚拟机》
https://blog.csdn.net/gavin_dyson/article/details/70832185
https://blog.csdn.net/u010002184/article/details/90748963
https://www.jianshu.com/p/eb30a388c5fc

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值