理解设计模式之:多层次的单例模式

多维度的单例模式

23种设计模式中,对于开发者而言,最熟悉不过就是单例模式了,单例模式的作用以及应用场景就不过多赘述了。今天我们的目的,主要是从多层次理解单例模式以及在实现单例模式中涉及的相关Java知识点。

  1. 饿汉式单例:

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

    这样一旦尝试通过反射去破坏单例时,就会抛出异常:

  2. 懒汉式一:

    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)形式的单例:

  3. 懒汉式二:

    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并发编程》一书的作者鄙视的,它认为这种优化是丑陋的。

  4. 静态内部类形式:

    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对象,那么静态内部类这种单例形式是做不到的。

  5. 序列化对于单例的影响:

    我们以饿汉式这种方式为例,看下面代码:

    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是在类加载的时候初始化的,只会初始化一次,这样就保证了我们通过反序列出来的对象和原来的是同一个。

  6. 通过枚举实现单例

    public enum  Singleton5 {
        INSTANCE;
    
        public void method() {
            System.out.println("method...");
        }
    }

    这样简简单单就实现了单例,而且不会有我们之前提到的反射攻击以及反序列化影响这些问题。

         参考文献:《深入理解Java虚拟机》《Android源码设计模式解析与实战》《Effective Java 3rd》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值