本文介绍了单例模式及其4种推荐写法(饿汉模式,双重校验锁(DCL),Holder模式(静态内部类)和枚举模式)和3类保护手段(反序列化,反射,自定义类加载器)
Ensure a class has only one instance,and provide a global point of access to it.
确保某一个类只有一个实例,并且自行实例化并向整个系统提供这个实例.
值得注意的是,单例往往都可以通过直接声明为static来实现,把一个实例方法变成静态方法,或者把一个实例变量变成静态变量,都可以起到单例的效果。这只是面向对象和面向过程的区别。
这里列举了单例模式的4种安全的推荐写法,另外也可以直接在获取单例的方法上加synchronized,这种方法虽然安全但效率极低,不值得推荐.这里4种推荐写法分别为饿汉模式,双重校验锁(DCL),Holder模式(静态内部类)和枚举模式.
public class Singleton {
private final static Singleton INSTANCE= new Singleton ();
private Singleton () {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
public class Singleton {
private volatile static Singleton instance = null;
private Singleton () {
}
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
public class Singleton {
private static class SingletonHolder {
public static final Singleton INSTANCE = new Singleton();
}
private Singleton (){
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
public enum Singleton {
INSTANCE;
}
单例写法 | 单例保障机制 | 单例对象初始化时机 | 优点 | 缺点 |
---|---|---|---|---|
饿汉模式 | 类加载机制 | 类加载 | 简单,易理解 | 难以保证懒加载,无法应对反射和反序列化 |
双重校验锁(DCL) | 锁机制(需volatile防止重排序) | 第一次调用getInstance() | 实现懒加载 | 复杂,无法应对反射和反序列化 |
Holder模式(静态内部类) | 类加载机制 | 第一次调用getInstance() | 实现懒加载 | 无法应对反射和反序列化 |
枚举 | 枚举语言特性 | 第一次引用枚举对象 | 简洁,安全(语言级别防止通过反射和反序列化破坏单例) | enum的另类用法 |
前三种写法在实现序列化(implements java.io.Serializable)时为保证单例都需要重写readResolve()方法,如
public class Singleton implements java.io.Serializable {
public final static Singleton INSTANCE = new Singleton();
protected Singleton() {
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
//反序列化时直接返回当前INSTANCE而不是反序列化出来的对象
private Object readResolve() {
return INSTANCE;
}
}
然后再通过例子看看枚举为什么不会受到反序列化的破解
public enum SingletonEnum {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
在枚举类型的序列化和反序列化上,Java做了特殊的规定:
在序列化时Java仅仅是将枚举对象的name属性输出到结果中,
反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。
同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性
前三种写法默认无法应对反射(先把私有的构造器设为accessible,再用newInstance()方法),但newInstance()也是通过new来获得实例的,所以可以从构造方法入手,进行保护,如
public class Singleton implements java.io.Serializable {
public final static Singleton INSTANCE = new Singleton();
//初始化标识
private static volatile boolean flag = true;
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
//在构建实例之前判断是否已经初始化过了,如果初始化过一次构造方法仍被调用则抛出异常
private Singleton(){
if(flag){
flag = false;
}else{
throw new RuntimeException("The instance already exists !");
}
}
}
再解释一下枚举单例不受反射破坏的原因,直接看源码:
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);
}
//这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
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;
}
说明确实无法使用反射创建枚举实例
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性
—— 《深入理解Java虚拟机》 第7章- 虚拟机类加载机制
如果是使用自定义的类加载器进行加载,就可以产生新的实例.(枚举也无能为力)
假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
这里有个获得单例类对象的方法
private static Class getClass(String classname)
throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
}
这段代码目的是每次获取的时候能获得同一个类加载器,然后再通过这个加载器去加载单例类来保证唯一性,所以这只是提供了获取同一个类加载器加载的单例类的途径,而非提供保护.
但是,从另一个方面来理解,使用不同的类加载器获得的本身就不是同一个类了,不同的类的实例仍然各只有一个,从这个角度看单例模式也不算被破坏,总不能要求两个不同的类的实例之和只能为1吧.
参考:
http://cantellow.iteye.com/blog/838473
https://zhuanlan.zhihu.com/p/32310340
https://blog.csdn.net/javazejian/article/details/71333103
《深入理解Java虚拟机》–周志明