多维度的单例模式
23种设计模式中,对于开发者而言,最熟悉不过就是单例模式了,单例模式的作用以及应用场景就不过多赘述了。今天我们的目的,主要是从多层次理解单例模式以及在实现单例模式中涉及的相关Java知识点。
-
饿汉式单例:
public class Singleton { private static final Singleton sInstance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return sInstance; } }
最简单的实现方式,也不会有啥线程安全的问题。但是禁不住反射的攻击:
public class Test { public static void main(String[] args) { Singleton instance = Singleton.getInstance(); Class<Singleton> clazz = Singleton.class; try { Constructor<Singleton> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); Singleton singleton = constructor.newInstance(); System.out.println(singleton == instance); //false } catch (Exception e) { e.printStackTrace(); } } }
在这段测试代码中,我们的Singleton类已经不能保证是单例了。怎么解决这个问题呢?
改动我们的Singleton构造器:
private Singleton() { if (sInstance != null) { throw new RuntimeException("There is only one instance of Singleton "); } }
这样一旦尝试通过反射去破坏单例时,就会抛出异常:
-
懒汉式一:
public class Singleton2 { private static Singleton2 sInstance = null; private Singleton2() { } public static Singleton2 getInstance() { if (sInstance == null) { sInstance = new Singleton2(); } return sInstance; } }
众所周知,这段代码是有线程安全问题的,即在多线程的环境下就不能保证单例了,因为if那块不是原子性操作。所以,才有了下面的双重校验锁(Double Check Lock, DCL)形式的单例:
-
懒汉式二:
public static Singleton2 getInstance() { if (sInstance == null) { synchronized (Singleton2.class) { if (sInstance == null) { sInstance = new Singleton2(); } } } return sInstance; }
这里给出的是基于同步代码块形式的,基于同步方法形式就省略了。这个地方就一个关键点,就是这个锁对象是谁,曾经多个面试者在手写代码时,这个地方的锁对象千奇百怪。我们这里用的是Singleton2类的Class对象,对于Class对象: 是类加载器查找并加载.class文件,然后保存到方法区,并且在Java堆内存中会创建一个Class对象来代表这个.class文件。不管这个类产生了多少个实例,它对应的Class对象只有一个。
但是这种方式真的是ok的吗?对于sInstance = new Singleton2()这条语句而言,它并不是原子操作,我们可以大致拆分为三步:
- 给Singleton2的实例分配内存
- 调用Singleton2的构造函数,初始化成员字段
- 将sInstance指向分配的内存空间
正式由于这一点,在JDK1.5以前,第二条和第三条的顺序是无法保证的。如果第三条先执行,而第二条没有得到执行就被切换到另一个线程,这时该线程再去访问getInstance方法获取实例时,由于sInstance已经非空了,再去使用时就会报错。那么这个问题又如何去解决呢?这里需要提到一个关键字:volatile。关于volatile关键字,它有两个作用:
- 禁止指令重排序,所以它能保证有序性
- 当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存中,所以它对其他线程是可见的,所以它能保证可见性
注意,volatile是不能够保证原子性的。至于什么是原子性,最初接触这个概念时是在学校学习操作系统时,会有原子操作的概念,即原子操作是不会被中断的,cpu在执行原子操作时是不会让出执行权的。对于JVM而言,也是如此。我们可以看下面几条语句:
x = 5; // 原子操作
y = x; // 非原子操作,它包括从x中读和往y中写两个操作
x++; // 非原子操作,需要先去读取x的值,然后执行自增,最后再写入x
对于更多内容大家可以去阅读《深入理解Java虚拟机》一书。回到我们的Singleton2类来,我们就有如下改动:
private volatile static Singleton2 sInstance = null;
实际上这种双重校验锁的方式是被《Java并发编程》一书的作者鄙视的,它认为这种优化是丑陋的。
-
静态内部类形式:
public class Singleton3 { private Singleton3() { } public static Singleton3 getInstance() { return Holder.SINGLETON; } private static class Holder { private static final Singleton3 SINGLETON = new Singleton3(); } }
静态内部类这种形式有很多优点:
- 它是线程安全的
- 它是一种懒加载,即只有在第一次调用getInstance()方法才会导致类加载器去加载Singleton3.Holder()类,才会去初始化SINGLETON,这个时候才会去创建Singleton3的实例
但是这种方式也有不足之处(饿汉式也有),那就是在创建对象时传递不了参数。例如Android中,我在某个单例类在创建对象时需要一个Context对象,那么静态内部类这种单例形式是做不到的。
-
序列化对于单例的影响:
我们以饿汉式这种方式为例,看下面代码:
public class Singleton4 implements Serializable { private static final Singleton4 sInstance = new Singleton4(); private int id = 25; private Singleton4() { } public static Singleton4 getInstance() { return sInstance; } }
编写一个测试main方法:
Singleton4 beforeInstance = Singleton4.getInstance(); FileOutputStream fos = new FileOutputStream("test.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(beforeInstance); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("test.txt"); ObjectInputStream ois = new ObjectInputStream(fis); Singleton4 afterInstance = (Singleton4) ois.readObject(); System.out.println(beforeInstance == afterInstance); // false
最终会打印false,这也就意味着单例再次被破坏了。前面我们面对反射的攻击时,通过抛异常的形式解决了,那么这里该怎么办?在Singleton4类中添加如下代码:
private Object readResolve() { return sInstance; }
再次运行上面的测试main方法,它就变为true了,也就意味着我们面对攻击时再次保证了单例。但是我想问为什么?那就得从ObjectInputStream的readObject()方法看起:
public final Object readObject(){ ... try { Object obj = readObject0(false); } ... }
查看readObject0():
private Object readObject0(boolean unshared) throws IOException{ ... try { switch (tc) { case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); } }
分析流程就到了readOrdinaryObject():
private Object readOrdinaryObject(boolean unshared){ ObjectStreamClass desc = readClassDesc(false); 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) { // Filter the replacement object if (rep != null) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1); } } handles.setObject(passHandle, obj = rep); } } return obj; }
贴出的代码对于我们的分析至关重要,先看readClassDesc():
private ObjectStreamClass readClassDesc(boolean unshared){ switch (tc) { case TC_CLASSDESC: descriptor = readNonProxyDesc(unshared); } private ObjectStreamClass readNonProxyDesc(boolean unshared){ try { totalObjectRefs++; depth++; desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false)); } finally { depth--; } } void initNonProxy(ObjectStreamClass model, Class<?> cl, ClassNotFoundException resolveEx, ObjectStreamClass superDesc){ if (cl != null) { osc = lookup(cl, true); } static ObjectStreamClass lookup(Class<?> cl, boolean all){ if (entry == null) { try { entry = new ObjectStreamClass(cl); } catch (Throwable th) { entry = th; } if (future.set(entry)) { Caches.localDescs.put(key, new SoftReference<Object>(entry)); } else { // nested lookup call already set future entry = future.get(); } }
这里会调用ObjectSteamClass的有参构造,在这个构造器中,有一行至关重要的代码:
readResolveMethod = getInheritableMethod( cl, "readResolve", null, Object.class);
这个readResolveMethod这个Method对象就代表了我们在单例类添加的readResolve方法。找到了它,我们再回头看readOrdinaryObject()方法:
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) { // Filter the replacement object if (rep != null) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1); } } handles.setObject(passHandle, obj = rep); } } return obj;
看这里的if条件:desc.hasReadRessolveMethod():
boolean hasReadResolveMethod() { requireInitialized(); return (readResolveMethod != null); }
只要我们往序列化类中添加了readResolve方法,这个readResolveMethod对象就不为空;然后desc.invokeReadResolve()实际上就是通过反射调用我们提供的readResolve方法,最终将该方法的返回作为readObject()的返回值。由于我们的readResolve方法是直接返回了sInstance,而sInstance是在类加载的时候初始化的,只会初始化一次,这样就保证了我们通过反序列出来的对象和原来的是同一个。
-
通过枚举实现单例
public enum Singleton5 { INSTANCE; public void method() { System.out.println("method..."); } }
这样简简单单就实现了单例,而且不会有我们之前提到的反射攻击以及反序列化影响这些问题。
参考文献:《深入理解Java虚拟机》《Android源码设计模式解析与实战》《Effective Java 3rd》