学习了《Android 源码设计模式解析与实战》作者的Android源码设计模式分析项目以及最适合作为Java基础面试题之Singleton模式两篇优秀文章,来写个笔记,方便复习。
单例是我学习的第一个设计模式,今天系统的回顾下:
单例顾名思义就是让某一类型只允许存在单独一个实例.
public class LazySingleton {
private volatile static LazySingleton INSTANCE = null;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (INSTANCE == null)
INSTANCE = new LazySingleton();
return INSTANCE;
}
}
此方法需要使用synchronized来避免并发创建多个实例,耗资源,不佳。
public class LazySingletonWithDoubleCheck {
private volatile static LazySingletonWithDoubleCheck INSTANCE = null;
private LazySingletonWithDoubleCheck() {
}
public static LazySingletonWithDoubleCheck getInstance() {
if (INSTANCE == null) {
synchronized (LazySingletonWithDoubleCheck.class) {
if (INSTANCE == null)
INSTANCE = new LazySingletonWithDoubleCheck();
}
}
return INSTANCE;
}
}
这是Lazy Singleton的改良版,这种采用了double-check的实现方式避免了对getInstance方法总是加锁。此举即保证了线程安全,又将性能折损明显降低了,不失为比较理想的做法。
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {
}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
如果该类实例化的开销比较大,这种方式或许就不太理想,不过它无需担心多线程同步获取该实例时可能出现的并发问题。
public class InnerClassSingleton {
private static class SingletonHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
private InnerClassSingleton() {
}
public static final InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
另外一种可以有效解决线程并发问题并延迟实例化的做法就是如上代码所示的利用静态内部类来实现。单例类的实例被包裹在内部类中,因此该单例类加载时并不会完成实例化,直到有线程调用getInstance方法,内部类才会被加载并实例化单例类。这种做法应该说是比较令人满意的。
(利用静态内部类不会立马加载 与 只会加载一次 这辆特性,实现延时、单例)
public class ClonableSingleton implements Cloneable{
private static final ClonableSingleton INSTANCE = new ClonableSingleton();
private ClonableSingleton() {
}
public static ClonableSingleton getInstance() {
return INSTANCE;
}
public Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
Java中类通过实现Clonable接口并覆写clone方法就可以完成一个其对象的拷贝。而当Singleton类为Clonable时也自然无法避免可利用这种方式被重新创建一份实例。
// 测试
public static void checkClone() throws Exception {
ClonableSingleton a = ClonableSingleton.getInstance();
ClonableSingleton b = (ClonableSingleton) a.clone();
assertEquals(a, b);
}
public class SerializableSingleton implements Serializable{
private static final long serialVersionUID = 6789088557981297876L;
private static final SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton() {
}
public static SerializableSingleton getInstance() {
return INSTANCE;
}
}
第二种破坏方式就是利用序列化与反序列化,当Singleton类实现了Serializable接口就代表它是可以被序列化的,该实例会被保存在文件中,需要时从该文件中读取并反序列化成 对象。可就是在反序列化这一过程中不知不觉留下了可趁之机,因为默认的反序列化过程是绕开构造函数直接使用字节生成一个新的对象。于是,Singleton在反序列化时被创造出第二个实例。通过如下代码可轻松实现这一行为,a与b最终并不相等。
// 测试
public static void checkSerialization() throws Exception {
File file = new File("serializableSingleton.out");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(
file));
SerializableSingleton a = SerializableSingleton.getInstance();
out.writeObject(a);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
SerializableSingleton b = (SerializableSingleton) in.readObject();
in.close();
assertEquals(a, b);
}
public static void checkReflection() throws Exception {
EagerSingleton a = EagerSingleton.getInstance();
Constructor cons = EagerSingleton.class
.getDeclaredConstructor();
cons.setAccessible(true);
EagerSingleton b = (EagerSingleton) cons.newInstance();
assertEquals(a, b);
}
前两种破坏方式说到底都是通过避免与私有构造函数正面冲突的方式另辟蹊径来实现的,而这种方式就显得艺高人胆大,既然你是私有的不允许外界直接调用,那么我就利用反射机制强行逼你就范:公开其访问权限。如此一来,原本看似安全的堡垒顷刻间沦为炮灰,Singleton再次沦陷。
public static void checkClassloader() throws Exception {
String className = "fernando.lee.singleton.EagerSingleton";
ClassLoader classLoader1 = new MyClassloader();
Class> clazz1 = classLoader1.loadClass(className);
ClassLoader classLoader2 = new MyClassloader();
Class> clazz2 = classLoader2.loadClass(className);
System.out.println("classLoader1 = " + clazz1.getClassLoader());
System.out.println("classLoader2 = " + clazz2.getClassLoader());
Method getInstance1 = clazz1.getDeclaredMethod("getInstance");
Method getInstance2 = clazz2.getDeclaredMethod("getInstance");
Object a = getInstance1.invoke(null);
Object b = getInstance2.invoke(null);
assertEquals(a, b);
}
Java中一个类并不是单纯依靠其全包类名来标识的,而是全包类名加上加载它的类加载器共同确定的。因此,只要是用不同的类加载器加载的Singleton类并不认为是相同的,因此单例会再次被破坏,通过自定义编写的MyClassLoader即可实现。
public class SafeSingleton implements Serializable, Cloneable {
private static final long serialVersionUID = -4147288492005226212L;
private static SafeSingleton INSTANCE = new SafeSingleton();
private SafeSingleton() {
if (INSTANCE != null) {
throw new IllegalStateException("Singleton instance Already created.");
}
}
public static SafeSingleton getInstance() {
return INSTANCE;
}
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("Singleton can't be cloned");
}
}
在原有Singleton的基础上完善若干方法即可实现一个安全的更为纯正的Singleton。注意到当实例已经存在时试图通过调用私有构造函数会直接报错从而抵御了反射机制的入侵; 让调用clone方法直接报错避免了实例被克隆;覆写readReslove方法直接返回现有的实例本身可以防止反序列化过程中生成新的实例。而对于不同类加载器导致的单例模式破坏笔者暂 未亲测出切实可行的应对方案,还烦请大牛提供高见。
(不同类加载器仍然可以绕过去)
public enum EnumSingleton{
INSTANCE;
private EnumSingleton(){
}
}
采用枚举的方式实现Singleton非常简易,而且可直接通过EnumSingleton.INSTANCE获取该实例。Java中所有定义为enum的类内部都继承了Enum类,而Enum具备的特性包括类加载是静态的来保证线程安全,而且其中的clone方法是final的且直接抛出CloneNotSupportedException异常因而不允许拷贝,同时与生俱来的序列化机制也是直接由JVM掌控的并不会创建出新的实例,此外Enum不能被显式实例化反射破坏也不起作用。当然它也不是没有缺点,比如由于已经隐式继承Enum所以无法再继承其他类了(Java的单继承模式限制),而且相信大多数人并不乐意仅仅为了实现一个纯正的Singleton就将更为习惯的class修改为enum。