Android开发设计模式Android开发

【设计模式】- 单例模式

2019-06-22  本文已影响6人  拔萝卜占坑

简介

单例模式可以说在平时开发过程中经常用到。当整个应用进程只需要创建一次某对象时。那么单例模式就派上用场了。很多人觉得单例模式很简单,但是里面有些细节和不同写法的差别,以及不同写法都解决了什么问题。还是可以了解了解。这样在开发过程中可以根据实际情况来使用单例模式。

UML

【设计模式】- 单例模式.png

难点

单例,单例——那么重点就是如何保证单例类的对象在整个应用进程有且只有一个。比如在多线程情况下,异步和原子操作带来的执行顺序的差异等等。

单例1

这种算是最简单的一种写法,利用虚拟机只会对类加载一次,而静态变量会在类加载时进行初始化来保证单例类对象在整个应用进程期间有且只有一个。但是缺点就是:类加载便会立即创建单例对象,不管单例对象后面是否会被使用,会导致一定的资源浪费。

反序列化导致单例模式失效

让Singleton实现Serializable接口。编写测试程序。

private static void testSingleton() {

    Singleton singleton = Singleton.instance();
    try {
        FileOutputStream fos = new FileOutputStream(new File("singletonTest.txt"));
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(singleton);
        fos.close();
        oos.close();
        System.out.println("old:" + singleton.hashCode());

        FileInputStream fis = new FileInputStream(new File("singletonTest.txt"));
        ObjectInputStream ois = new ObjectInputStream(fis);
        Singleton newSingleton = (Singleton) ois.readObject();
        fis.close();
        ois.close();
        System.out.println("new:" + newSingleton.hashCode());

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

打印结果:

old:2016447921
new:960604060

可以发现,前后并不是同一个对象。那么是怎么造成的呢。看一下ObjectInputStream的readObject()方法。

    public final Object readObject()
        throws IOException, ClassNotFoundException
    ...
        try {
            Object obj = readObject0(false);
            ...
            return obj;
        } finally {
            ...
    }

这里obj便是序列化出来的对象。查看readObject0方法。

private Object readObject0(boolean unshared) throws IOException {
   ...
   try {
      switch (tc) {
          case TC_OBJECT:
             return checkResolve(readOrdinaryObject(unshared));
          ...
      }
   ...
}

序列化对象由readOrdinaryObject(unshared)获得,继续向下看。

    private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
       ...
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            ...
        }
        ...
        return obj;
    }

重点就在obj = desc.isInstantiable() ? desc.newInstance() : null这行代码了。

  1. isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。
  2. desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。
    所以如果不做任何处理,序列化出来的是新创建的对象。
解决办法

增加readReslove方法,该方法会在反序列化时调用,且返回一个对象,这个对象就是readObject返回的对象。

public class Singleton implements Serializable {

    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    public static Singleton instance() { return INSTANCE; }
    private Object readResolve() {
        System.out.println("readResolve被调用了");
        return INSTANCE;
    }
    private Object writeReplace() {
        System.out.println("writeReplace被调用了");
        return INSTANCE;
    }
}

打印结果

writeReplace被调用了
old:1229416514
readResolve被调用了
new:1229416514

单例2

class Singleton2{
    private static Singleton2 singleton2;
    private Singleton2() {}
    public static Singleton2 instance(){
        if (null == singleton2){
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}

这种但是模式不适合运用到多线程情况,当使用场景不是在多线程时,那么这种写法的性能比第三种单例写法高。

单例3(双重检测)

class Singleton3{
    private static Singleton3 singleton3;
    private Singleton3() {}
    public static Singleton3 instance(){
        if (null == singleton3){
            synchronized (Singleton3.class){
                if (null == singleton3){
                    singleton3 = new Singleton3();
                }
            }
        }
        return singleton3;
    }
}

这种方式有叫“双重检测”,即检测两次是否为null。并用synchronized解决多线程带来的问题,那么问什么用两次判null,为什么不把synchronized写到方法体或者最外层呢?

  1. 为什么不把synchronized写到方法体或者最外层呢?
    如果singleton3不为null,那么就不会执行为null情况下的同步代码,要知道代码同步需要获取锁,释放锁等操作,是个繁重的过程,所以这样可以提高效率。
  2. 为什么还要进行第二次判null?
    想象这种情况,有A,B两个线程同时调用instance获取对象。线程A获取锁进入第二层判null代码,进程创建单例对象,但是这个过程并不是原子操作,什么时候创建完,并不知道。在没有创建完成的情况下,线程B到达,由于单例对象还未创建成功,所以线程B到达时,顺利通过第一层判null,然后试图获取锁进入第二层判null,这是发现锁被A持有,那么等待A释放锁,当单例对象创建成功,A也释放锁,这是B进入同步代码块,如果不进行第二次判null,那么B也会重新创建一个对象。

那这种方式能够保证单例的唯一性吗?其实并不能,由于java内存模型的缘故,"双重检测"并不能保证单例的唯一性。先补一下“java内存模型”知识。

Java内存模型

参考《深入理解Java虚拟机》一书。

不完美的双重检测

有了上面的知识后,我们来试想一种场景。线程A,B同时调用instance获取实例,这时线程A,B的工作内存保存的是主内存中的副本,这是实例还没有创建,所有都为null,当线程A获得锁,进行实例创建,而B等待A释放锁,当A创建完对象,释放锁后,B获得锁进入第二层判null逻辑。重点来了,线程A的确完成了实例的创建,但是是在自己的工作内存,还没有同步到主内存,那么这时候线程B中的实例依然是null,所有还是会重新创建对象。解决办法可以给实例对象加上volatile关键字,下面补一下volatile关键字知识。

volatile

单例4(枚举)

enum Singleton4{
    INSTANCE;
    public void test(){}
}

kotlin中单例模式

kotlin创建单例就很简单了,利用object 关键字即可而且对象声明的初始化过程是线程安全的。

object Singleton5 {
    fun registerDataProvider(provider: DataProvider) {}
    val allDataProviders: Collection<DataProvider>
        get() = // ……
}
上一篇 下一篇

猜你喜欢

热点阅读