Java与单例模式

最近在阅读《Effective Java 》这本书,第3个条款专门提到了单例属性,并给出了使用单例的最佳实践建议。让我对这个单例模式(原本我以为是设计模式中最简单的一种)有了更深的认识。

单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。

单例的特点

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

单例模式的7种写法

单例模式的写法很多,涉及到了线程安全和性能问题。在这里我不重复介绍。这篇《单例模式的七种写法》写得很详细,博主也给出了每一种写法的优缺点。

但是,单例模式真的能够实现实例的唯一性吗?答案是否定的。

如何破坏单例

反射

有两种常见的方式来实现单例。他们的做法都是将构造方法设为私有,并导出一个公有的静态成员来提供对唯一实例的访问。在第1种方式中,成员是个final字段:

// Singleton with public final field
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public void leaveTheBuilding() { ... }
}

只调用私有构造函数一次,以初始化公共静态final字段elvi.instance。不提供公有的或者受保护的构造函数保证了全局唯一性:当Elvis类初始化的时候,仅仅只会有一个Elvis实例存在——不多也不少 。无论客户端怎么做都无法改变这一点,只不过我还是要警告一下 :授权的客户端可以通过反射来调用私有构造方法,借助于AccessibleObject.setAccessible方法即可做到 。如果需要防范这种攻击,请修改构造函数,使其在被要求创建第二个实例时抛出异常。

测试代码:

public class TestSingleton {
	
	/**
	 * 通过反射破坏单例
	 */
	@Test
	public void testReflection() throws Exception {
		/**
		 * 验证单例有效性
		 */
		Elvis elvis1 = Elvis.INSTANCE;
		Elvis elvis2 = Elvis.INSTANCE;
		
		System.out.println("elvis1 == elvis2 ? ===>" + (elvis1 == elvis2));
		System.err.println("-----------------");
		
		/**
		 * 反射调用构造方法
		 */
		Class clazz = Elvis.class;
		Constructor cons = clazz.getDeclaredConstructor(null); 
		cons.setAccessible(true);
		
		Elvis elvis3 = (Elvis) cons.newInstance(null);
		
		System.out.println("elvis1 == elvis3 ? ===> "
			+ (elvis1 == elvis3));
	}
}

运行结果:

Elvis Constructor is invoked!
elvis1 == elvis2 ? ===> true
elvis1 == elvis3 ? ===> false
-----------------
Elvis Constructor is invoked!

结论:

反射是可以破坏单例属性的。因为我们通过反射把它的构造函数设成可访问的,然后去生成一个新的对象。

改进版的单例写法:

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();

	private Elvis() { 
		System.err.println("Elvis Constructor is invoked!");
		if (INSTANCE != null) {
			System.err.println("实例已存在,无法初始化!");
			throw new UnsupportedOperationException("实例已存在,无法初始化!");
		}
	}

}

结果:

Elvis Constructor is invoked!
elvis1 == elvis2 ? ===> true
-----------------
Elvis Constructor is invoked!
实例已存在,无法初始化!

第2种实现单例模式的方法是,提供一个公有的静态工厂方法:

// Singleton with static factory
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE; }
    public void leaveTheBuilding() { ... }
}

所有调用Elvis类的getInstance方法,返回相同的对象引用,并且不会有其它的Elvis对象被创建。但同样有上面第1个方法提到的反射破坏单例属性的问题存在。

序列化和反序列化

如果对上述2种方式实现的单例类进行序列化,反序列化得到的对象是否是同一个对象呢?答案是否定的。
看下面的测试代码:
单例类:

public class Elvis implements Serializable {
	public static final Elvis INSTANCE = new Elvis();

	private Elvis() { 
		System.err.println("Elvis Constructor is invoked!");
	}

}

测试代码:

	/**
	 * 序列化对单例属性的影响
	 * @throws Exception 
	 */
	@Test
	public void testSerialization() throws Exception {
		Elvis elvis1 = Elvis.INSTANCE;
		FileOutputStream fos = new FileOutputStream("a.txt");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(elvis1);
        oos.flush();
        oos.close();
        
        Elvis elvis2 = null;
        FileInputStream fis = new FileInputStream("a.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        elvis2 = (Elvis) ois.readObject();
        
        System.out.println("elvis1 == elvis2 ? ===>" + (elvis1 == elvis2));
	}

结果是:

Elvis Constructor is invoked!
elvis1 == elvis2 ? ===>false

说明:

通过对序列化后的Elvis 进行反序列化得到的对象是一个新的对象,这就破坏了Elvis 的单例性。

《Effective Java》已经告诉我们,在单例类中提供一个readResolve方法就可以完成单例特性。这里大家可以自己去测试。

接下来,我们去看看Java提供的反序列化是如何创建对象的!

ObjectInputStream

对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的,那么带着刚刚的问题,分析一下ObjectInputputStream的readObject 方法执行情况到底是怎样的。

为了节省篇幅,这里给出ObjectInputStream的readObject的调用栈:
Java与单例模式_第1张图片
大家顺着此图的关系,去看readObject方法的实现。
首先进入readObject0方法里,关键代码如下:

switch (tc) {
	//省略部分代码
	
	case TC_STRING:
	case TC_LONGSTRING:
		return checkResolve(readString(unshared));

	case TC_ARRAY:
		return checkResolve(readArray(unshared));

	case TC_ENUM:
		return checkResolve(readEnum(unshared));

	case TC_OBJECT:
		return checkResolve(readOrdinaryObject(unshared));

	case TC_EXCEPTION:
		IOException ex = readFatalException();
		throw new WriteAbortedException("writing aborted", ex);

	case TC_BLOCKDATA:
	case TC_BLOCKDATALONG:
		if (oldMode) {
			bin.setBlockDataMode(true);
			bin.peek();             // force header read
			throw new OptionalDataException(
				bin.currentBlockRemaining());
		} else {
			throw new StreamCorruptedException(
				"unexpected block data");
		}

	//省略部分代码

这里就是判断目标对象的类型,不同类型执行不同的动作。我们的是个普通的Object对象,自然就是进入case TC_OBJECT的代码块中。然后进入readOrdinaryObject方法中。
readOrdinaryObject方法的代码片段:

private Object readOrdinaryObject(boolean unshared)
        throws IOException {
	//此处省略部分代码

	Object obj;
	try {
		obj = desc.isInstantiable() ? desc.newInstance() : null;
	} catch (Exception ex) {
		throw (IOException) new InvalidClassException(
			desc.forClass().getName(),
			"unable to create instance").initCause(ex);
	}

	//此处省略部分代码

	if (obj != null &&
		handles.lookupException(passHandle) == null &&
		desc.hasReadResolveMethod())
	{
		Object rep = desc.invokeReadResolve(obj);
		if (unshared && rep.getClass().isArray()) {
			rep = cloneArray(rep);
		}
		if (rep != obj) {
			handles.setObject(passHandle, obj = rep);
		}
	}

	return obj;
}

重点看代码块:

 Object obj;
 try {
      obj = desc.isInstantiable() ? desc.newInstance() : null;
  } catch (Exception ex) {
      throw (IOException) new InvalidClassException(
          desc.forClass().getName(),
          "unable to create instance").initCause(ex);
  }

这里创建的这个obj对象,就是本方法要返回的对象,也可以暂时理解为是ObjectInputStream的readObject返回的对象。

isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。
desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。

所以。到目前为止,也就可以解释,为什么序列化可以破坏单例了?即序列化会通过反射调用无参数的构造方法创建一个新的对象。

接下来再看,为什么在单例类中定义readResolve就可以解决该问题呢?还是在readOrdinaryObjec方法里继续往下看。

if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
{
	Object rep = desc.invokeReadResolve(obj);
	 if (unshared && rep.getClass().isArray()) {
	     rep = cloneArray(rep);
	 }
	 if (rep != obj) {
	     handles.setObject(passHandle, obj = rep);
	 }
}

这段代码也很清楚地给出答案了!
如果目标类有readResolve方法,那就通过反射的方式调用要被反序列化的类的readResolve方法,返回一个对象,然后把这个新的对象复制给之前创建的obj(即最终返回的对象)。那readResolve 方法里是什么?就是直接返回我们的单例对象。

public class Elvis implements Serializable {
	public static final Elvis INSTANCE = new Elvis();

	private Elvis() { 
		System.err.println("Elvis Constructor is invoked!");
	}

	private Object readResolve() {
	   return INSTANCE;
	}
}

所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

单元素枚举类型

第三种实现单例的方式是,声明一个单元素的枚举类:

// Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;
    public void leaveTheBuilding() { ... }
}

这个方法跟提供公有的字段方法很类似,但它更简洁,提供天然的可序列化机制和能够强有力地保证不会出现多次实例化的情况 ,甚至面对复杂的序列化和反射的攻击下。这种方法可能看起来不太自然,但是拥有单元素的枚举类型可能是实现单例模式的最佳实践。注意,如果单例必须要继承一个父类而非枚举的情况下是无法使用该方式的(不过可以声明一个实现了接口的枚举)。
我们分析一下,枚举类型是如何阻止反射来创建实例的?直接源码:
看Constructor类的newInstance方法。

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;
}

这行代码(clazz.getModifiers() & Modifier.ENUM) != 0 就是用来判断目标类是不是枚举类型,如果是抛出异常IllegalArgumentException("Cannot reflectively create enum objects"),无法通过反射创建枚举对象!很显然,反射无效了。

接下来,再看一下反序列化是如何预防的。依然按照上面说的顺序去找到枚举类型对应的readEnum方法,如下:


private Enum<?> readEnum(boolean unshared) throws IOException {
    if (bin.readByte() != TC_ENUM) {
        throw new InternalError();
    }

    ObjectStreamClass desc = readClassDesc(false);
    if (!desc.isEnum()) {
        throw new InvalidClassException("non-enum class: " + desc);
    }

    int enumHandle = handles.assign(unshared ? unsharedMarker : null);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(enumHandle, resolveEx);
    }

    String name = readString(false);
    Enum<?> result = null;
    Class<?> cl = desc.forClass();
    if (cl != null) {
        try {
            @SuppressWarnings("unchecked")
            Enum<?> en = Enum.valueOf((Class)cl, name);
            result = en;
        } catch (IllegalArgumentException ex) {
            throw (IOException) new InvalidObjectException(
                "enum constant " + name + " does not exist in " +
                cl).initCause(ex);
        }
        if (!unshared) {
            handles.setObject(enumHandle, result);
        }
    }

    handles.finish(enumHandle);
    passHandle = enumHandle;
    return result;
}

readString(false):首先获取到枚举对象的名称name。
Enum en = Enum.valueOf((Class)cl, name):再指定名称的指定枚举类型获得枚举常量,由于枚举中的name是唯一,切对应一个枚举常量。所以我们获取到了唯一的常量对象。这样就没有创建新的对象,维护了单例属性。

看看Enum.valueOf 的JavaDoc文档:

  • 返回具有指定名称的指定枚举类型的枚举常量。 该名称必须与用于声明此类型中的枚举常量的标识符完全匹配。 (不允许使用无关的空白字符。)

具体实现:

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

enumConstantDirectory():返回一个Map,维护着名称到枚举常量的映射。我们就是从这个Map里获取已经声明的枚举常量,通过这个缓存池一样的组件,让我们可以重用这个枚举常量!

总结

  1. 常见的单例写法有他的弊端,存在安全性问题,如:反射,序列化的影响。
  2. 《Effective Java》作者Josh Bloch 提倡使用单元素枚举类型的方式来实现单例,首先创建一个枚举很简单,其次枚举常量是线程安全的,最后有天然的可序列化机制和防反射的机制。

参考

《单例模式的七种写法》
《单例与序列化的那些事儿》
《Effective Java》

分享

我加入了张龙老师的知识星球《effective java 3rd》英文版的学习群。加入我们,一起开启程序员的英文学习之路。
Java与单例模式_第2张图片

你可能感兴趣的:(设计模式,Java基础)