美文网首页
关于单例使用的72种姿势

关于单例使用的72种姿势

作者: 叔只是要日天 | 来源:发表于2018-12-18 18:15 被阅读0次

    什么是单例

    ​ 简单的说,就是在整个程序的生命周期,只初始化一次,且整个内存里只存在一个实例的对象。就好像我们一生只能娶一次老婆,且这一次只能娶一个老婆,且如果没有什么意外,这个老婆会陪你到死……因为女性的生命通常会比男性长几年,IT界犹为如此(波哥不在此列,欲知波哥的事迹,请戳 http://www.toutiao.com/a6467327568092594702/?tt_from=weixin&utm_campaign=client_share&app=news_article&utm_source=weixin&iid=14682829201&utm_medium=toutiao_android&wxshare_count=1

    单例的几种写法

    1. 饿汉模式

      public class Singleton {
          private static final Singleton mInstance = new Singleton();
      
          private Singleton(){}
      
          public static Singleton getInstance() {
              return mInstance;
          }
      }
      

      ​ 上面的代码就是一个最简单的饿汉模式的单例。它声明了一个静态的成员变量,且这个成员变量被直接赋值成一个Singleton的实例。

      ​ 这种方式构造出来的单例是线程安全的,基于以下原因:

      ​ 1.它的实例化是通过它的静态成员变量进行,

      ​ 2.一个类的静态成员变量会在类被加载的时候进行初始化,

      ​ 3.一个类只会被加载一次,不管有多少个线程同时触发类的加载

      ​ 需要注意的是,上面说的是“类加载”的时候,而不是类被实例化的时候,这两者是有区别的。类在被实例化的时候一定会被加载,但是类的加载并不一定需要实例化。据我所知,除了使用new关键字之外,至少在以下两个场景,这个类会被加载 :

      ​ 1.一个类被Class.forName(name)这种方法调用

      ​ 2.一个类有静态的方法,当这个方法被调用时。(类的静态成员常量被调用时不会引发类的加载)

      ​ 综上我们可以得出以下结论:

      ​ 1.饿汉模式是线程安全的

      ​ 2.饿汉模式里的单例,有可能在我们不知情的情况下被创建

      ​ 3.如果这个单例的初始化是耗时的(比如读取数据库),由于我们可能在不知情的情况下创建它,就有可能在主线程去做这个事情。

      ​ 4.当然这种用法在绝大多数的情况下是没有问题的_

    2. 懒汉模式

      public class LazySingleton {
          private static LazySingleton mInstance;
          
          private LazySingleton(){}
          
          public static LazySingleton getInstance() {
              if (mInstance == null) {
                  mInstance = new LazySingleton();
              }
              return mInstance;
          }
      }
      

      ​ 懒汉模式的特点是,它是在需要的时候才会创建,这样我们就能很明确地知道,它是什么时候会被实例化。也就是说,它的实例化是可知可控的。

      ​ 但是很明显,这种写法有线程安全上的问题,当多个线程同时去创建这个实例的时候,有可能不同线程创建出不同的实例,也就是说,这个单例实际上并不单。

    3. 同步的懒汉模式

      public class LazySingleton {
          private static LazySingleton mInstance;
          
          private LazySingleton(){}
          
          public synchronized static LazySingleton getInstance() {
              if (mInstance == null) {
                  mInstance = new LazySingleton();
              }
              return mInstance;
          }
      }
      

      ​ 基于懒汉模式的缺点,我们很容易想到加锁这个方法,就是在getInstance方法上加个同步锁,简单粗暴,但是疗效显著。但是synchronized一直被我们不喜,特别是加在方法上面,显得代码很不优雅,逼格不高,因此可能有人想到一种改进的的锁:下面的代码里,把锁移到方法体内,且只有mInstance为null的时候才加,看上去把逼格提升了一个档次。

      public class LazySingleton {
          private static LazySingleton mInstance;
      
          private LazySingleton(){}
      
          public static LazySingleton getInstance() {
              if (mInstance == null) {
                  synchronized (LazySingleton.class) {
                      mInstance = new LazySingleton();
                  }
              }
              return mInstance;
          }
      }
      

      ​ 但是这种做法貌似提升逼格,实际上引进了问题,我们想像一下这种场景:

      ​ 1.A线程获取单例,此时mInstance为null,A线程锁住对象,并进行初始化

      ​ 2.当A线程正在初始化单例的时候,B线程也请求获取单例 ,但是由于此时A线程未释放锁,B只能等待。

      ​ 3.A线程搞定了这个对象,带着对象回家了,并释放了锁

      ​ 4.B线程重新加锁,生成新的对象

      ​ 从上述过程很容易就看出,在这种场景下两个线程生成的是不同的对象,单例又不单了。鉴于此,我们想到了逼格更高的写法:双重检查锁。

    4. 双重检查锁的懒汉模式

      public class DoubleCheckSingleton {
          private static DoubleCheckSingleton mInstance;
      
          private DoubleCheckSingleton(){}
      
          public static DoubleCheckSingleton getInstance() {
              if (mInstance == null) {
                  synchronized (DoubleCheckSingleton.class) {
                      if (mInstance == null){
                          mInstance = new DoubleCheckSingleton();
                      }
                  }
              }
              return mInstance;
          }
      }
      

      ​ 我们会发现,与同步的懒汉模式相比,双重检测的写法是在同步块里多了一次判空,这样就避免了上述场景中B获得锁之后,又重新生成新的对象的问题。

      ​ 但是这样做都没有问题了吗?——没有错,在这里叔可以负责任的告诉大家,这样子还是有问题的。这个时候我们要拿放大镜去看了……问题在于“mInstance = new DoubleCheckSingleton();”这一句。在我们看来,这是一个原子操作,创建一个新的对象,赋值给mInstance 。但是在 Java 虚拟机中运行最终编译成生的指令的时候,它有三个步骤:

      ​ 1.分配一块内存给新生成的对象 (allocate memory)

      ​ 2.执行初始化对象的操作 (new DoubleCheckSingleton())

      ​ 3.把mInstance 指向对应的内存 (mInstance = new DoubleCheckSingleton())

      ​ 如果程序是这样执行的,那完全没有问题,但是问题是上面的指令有可能会重排(主要是为了在执行程序 时提高性能,有兴趣的同步可以戳 http://www.infoq.com/cn/articles/java-memory-model-1)。所以上面的3条指令经过重排可能变成下面这样子:

      ​ 1.分配一块内存给新生成的对象 (allocate memory)

      ​ 2.把mInstance 指向对应的内存 (mInstance = new DoubleCheckSingleton())

      ​ 3.执行初始化对象的操作 (new DoubleCheckSingleton())

      ​ 在单线程的情况下,上述两种执行顺序都没有问题,都能构造出一个实例,并赋给mInstance ,但是在多线程的情况下就有问题了。

      ​ 在这里我讲个对象还没生出来的故事吧:

       话说有一天我跟群爷说:群爷,叔给你介绍个对象吧。
       群爷说:就你这样的能有什么好货色?
       叔就拿出张照片:look,肤白,貌美,温柔,贤惠,关键是有肉,走起路来duang duang duang的
       群爷口水都流出来了————你们要知道群爷是一个三十几年的老处男,左右女朋友都已经满是茧了,在这种事情不淡定也是正常的:妹纸在哪?什么时候约出来吃个饭吧?
       然后叔告诉他:照片是妹纸的妈,妹纸还没生出来,大概要再等个二十来年。
      

      ​ 我们想像这样一种极端的场景:当A线程执行到上面的2的时候,B线程进来获取单例对象,由于此时这个对象已经被分配了,因此mInstance != null,于是B就很高兴地拿着mInstance走了,而实际上这个时候3还没有执行,也就是意味着B其实拿到的是一个假的DoubleCheckSingleton的实例,特别 是在DoubleCheckSingleton的实例化过程比较耗时的情况下,很可能出现B拿着假的对象去做羞羞的事情了。我们可以想像一下,当B裤子都脱了的时候才发现这个对象是假的,这是何等的卧槽。为了避免这种情况,我们可以给上面的代码再加点小手术:我们给mInstance加上volatile修饰符,这个修饰符除了强制变量的线程可见性之外,在这里还有一个更重要的作用,就是告诉编译器:介绍对象这个事,顺序不要给老子乱调啊~先确定已经生出来养大了再介绍给我啊!

      public class DoubleCheckSingleton {
          private volatile static DoubleCheckSingleton mInstance;
      
          private DoubleCheckSingleton(){}
      
          public static DoubleCheckSingleton getInstance() {
              if (mInstance == null) {
                  synchronized (DoubleCheckSingleton.class) {
                      if (mInstance == null){
                          mInstance = new DoubleCheckSingleton();
                      }
                  }
              }
              return mInstance;
          }
      }
      
    5. 静态内部类懒汉模式

      ​ 使用双重检测锁的方式去实现一个单例,从功能、性能、安全等各个角度来考量的话,都是没有问题的,它最大的问题是代码量。我们可以看到,为了实现这样一个单例,写了一大坨的代码。一个类这样写还好,两个类也可以接受,但是十几二十个类都这样写的话,就很ugly了。所以懒惰的程序员们想出了下面这样一个实现方式:

      public class InnerStaticClassSingleton {
          private static class HelperHolder {
              static final InnerStaticClassSingleton helper = new InnerStaticClassSingleton();
          }
      
          public static InnerStaticClassSingleton getInstance() {
              return HelperHolder.helper;
          }
      }
      

      ​ 这个单例的实现方法明显逼格更高一些……以更少的代码量实现了懒加载、线程安全,且兼顾了性能。我们看到它是通过一个静态的内部类的静态成员变量来实现对象的实例化。我们已经知道一个类的静态成员变量在加载它的时候会被初始化,而一个类的静态内部类是被用到的才会被加载(这一点和静态成员变量有着很大的不同),所以这种实现可以在需要的时候才去生成单例,且它的单例是在类(静态内部类)的加载时实例化,保证了在多线程上的安全,最后它什么锁也没有,性能杠杠的。

      ​ 当然如果硬要说它有什么毛病的话,还是有的……比如:

      ​ 1.序列化会破坏单例模式,因为对象在被反序列化的时候会生成一个新的对象……为毛要把一个单例去序列化?这不是脑子有坑吗?因为单例在哪都能访问到啊!所以叔唯一能想到可能的场景就是某个对象里面持有一个接口,而这个接口的某个实现是一个单例,当我们去序列化-反序列化这个对象的时候,是会把这个单例同时序列化-反序列化的。当然如果真的出现问题,也是可以解决,就是反序列化的时候 直接返回那个静态成员变量。

      ​ 2.通过反射强行实例化一个单例对象。都 知道是单例了还强化反射去实例化?这是秀技术还是秀脑坑啊……所以一般,通常,正常人是不会干这事的吧……当然如果你非得防着这一手,也可以,在构造函数里加个判断,如果已经有了实例了直接抛个异常出去。

    6. 枚举单例

      public enum  EnumInstance {
          INSTANCE;
          
          public void doSomething(){}
      }
      

      ​ 如上面的代码,最简单,最优雅,最高逼格的单例实现,懒加载、线程安全、高性能、序列化安全(枚举常量不管怎么序列化反序列化返回的都是同一个实例)、反射安全(枚举类不能通过反射实例化)。当然它也不是十全十美的,比如更占内存(其实以我们的层次,似乎还到不了考虑这点内存开销的程度),比如无法继承与被继承,但是相对于它的优点来说,这些都不是事儿~

    为什么要使用单例

    ​ 关于这个问题,可以说很多……比如可以提高效率啊,比如线程共享啊,比如全局访问啊,每一点都有足够多支持它的理由。所以叔总结了一点,就是用起来很爽。

    单例与静态类的区别

    ​ 这个问题其实困扰了叔很久。单例和静态类有什么区别?单例是通过一个静态对象访问其方法和成员,而静态类是直接通过静态方法和静态成员访问,随便拿出一个单例来,基本上改吧改吧都能改成静态类,而且调用起来还更方便,直接xxx.xx(),而不是xxx.getInstance().xx()。上面提到我们为什么要用单例,说它效率高(只实例化一次),线程共享,全局访问等等,这些其它静态类都可以做到,那为什么我们经常用单例而不是静态类?叔搜索了一下其他人的看法,比较靠谱的大概有:

    ​ 1.静态类更多的是面向过程开发,单例则是面向对象开发

    ​ 2.单例可以继承、被继承,可以实现接口,而静态类则做不到

    ​ 3.单例可以懒加载,而静态类则做不到。这个和饿汉模式一样的,因为它的成员变量都是静态的,因此都会在类被加载 的时候 被初始化,而这个类的加载是不可预知的。

    单例的管理

    ​ 郑重声明:以下纯属个人见解,不一定对,只是提供一种思路,如有雷同,那绝B是缘份。

    ​ 单例用起来是用爽,但是当代码中的单例越来越多,不可避免地产生一个问题:如何优雅地管理这些单例?

    ​ 1.现在的程序规模越来越大,一个系统 可能有几个,几十个,甚至 几百个程序员参与其中,有些人用这种单例的实现,有些人用那种单例的实现,通过上面的分析,我们知道有些实现 是有问题的,而我们不能保证所有人都能懂得正确的实现方式。

    ​ 2.要实现一个没有什么问题的单例,要写不少的代码(枚举单例除外),当单例写得多了,这些重复的代码也变多了,对于一个有代码洁癖的人(比如叔),这是难以忍受的事。

    ​ 3.当然我们可以用枚举单例,但是同样如上文所述,枚举单例也有它的缺陷,也有它做不到的。比如有一个缓存类,这个类是继承于HashMap(主要目的是减少存、取等相关处理的代码,可以利用既有的实现),并在此基础上增加一些自己的方法,这种情况下用枚举单例就做不到了。另外其实枚举单例这种东西,不太容易接受……比如叔写多了getInstance()后第一次见到枚举单例的时候,立马卧了个槽,这特么的什么鬼代码?

    ​ 4.单例中不可避免地存在一些数据,当某种情况下我们需要去清理这些数据的时候,比如切换帐号,要把内存里的消息啊、会话给清掉。如果我们有几十个需要清理数据的单例,那么会出现一种什么情况呢?见代码:

    public void stopIM(){
        A.getInstance().clear();
        B.getInstance().clear(); 
        ....
        Z.getInstance().clear();
    }
    

    ​ 或者说,如果我们忘了调某个单例的clear()方法呢?

    ​ 所以在漫长的实践过程中,叔曾做过这样的改良(?):

    public enum  SdkInstanceHolder {
        INSTANCE;
        private ITransportOperation mTransportOperation;
        private Xx mXx;
        .....
    
        /**
         * 获取传输层操作接口
         * @return 传输层操作接口
         */
        public ITransportOperation getTransportOperation() {
            if (mTransportOperation == null) {
                mTransportOperation = new TransportOperation(AppFactory.instance().getApplicationContext());
            }
            return mTransportOperation;
        }
        
        public Xx getXx(){
            if (mXx == null){
                mXx = new Xx();
            } 
            return mXx;
        }
        
        ......
    
        public void clear(){
            mTransportOperation = null;
            ......
        }
    }
    

    ​ 像上面这个代码,通过一个枚举单例来管理所有其它的单例,这样可以省去很多的创建单例代码,是不是很屌?实际上上面的代码很有问题,首先在线程安全上它就是不过关的,如果多个线程同时取某个单例,很可能会取出不同的对象。其次每当我需要增加一个单例的时候,这个类就需要增加一个成员变量,一个方法,另外在clear的代码还是省不了。也就是说,看上去它解决了一部分的问题,实际上它不但没有解决问题,反而增加了风险——实际上这样的代码现在还在叔的sdk里,之所以目前还没出问题,只不过是因为它管理的东西太少太简单,没有把问题暴露出来而已。

    ​ 那么这个问题是无解的吗?当然不是!在叔这种世界级的程序员的字典里没有“无解”这两个字!

    ​ 通过一个类来管理单例必须可行,首先先解决每增加一种单例就要增加一个方法的问题:

    public class SingleInstance {
        private static ConcurrentHashMap<Class,Object> mInstanceMap = new ConcurrentHashMap<>();
        public static  <T> T get(Class<T> t){
            Object o = mInstanceMap.get(t);
            if (o != null) {
                return (T) o;
            }
            return IMReflectUtils.createNoArgumentInstanceFromClass(t);
        }
    }
    

    ​ 我们通过泛型来解决这个问题,在SingleInstance这个静态类里持有一个map,存放了单例类和已经创建出来的单例对象,当我们要取某个类的单例的时候,先去map里面取,没有的话就去new一个然后丢到map里。当然这样是不行的,因为它不是线程安全的,所以还得改:

    public class SingleInstance {
        private static ConcurrentHashMap<Class,Object> mInstanceMap = new ConcurrentHashMap<>();
        public static  <T> T get(Class<T> t){
            Object o = mInstanceMap.get(t);
            if (o != null) {
                return (T) o;
            }
            synchronized (t) {
                o = mInstanceMap.get(t);
                if (o != null) {
                    return (T) o;
                }
                T instance = IMReflectUtils.createNoArgumentInstanceFromClass(t);
                mInstanceMap.put(t,instance);
                return instance;
            }
        }
    }
    

    ​ 像上面这样,我们发现某个单例未实例化的时候,就对该单例的类加个锁,避免其它线程也去创建同一个类的单例。在获取锁之后再判断一次是不是已经实例化过了,因为有可能其它线程已经把单例创建出来了,如果还是没有,那才去创建新的单例,同样地创建完成之后丢到map里。到这里,已经基本解决了不断增加方法的问题了,那么切换帐号清数据怎么办呢?要写无数的clear的那个问题还得解决。

    public interface Clearable{
        void clear();
    }
    
    public class SingleInstance {
        private static ConcurrentHashMap<Class,Object> mInstanceMap = new ConcurrentHashMap<>();
        public static  <T> T get(Class<T> t){
            Object o = mInstanceMap.get(t);
            if (o != null) {
                return (T) o;
            }
            synchronized (t) {
                o = mInstanceMap.get(t);
                if (o != null) {
                    return (T) o;
                }
                T instance = IMReflectUtils.createNoArgumentInstanceFromClass(t);
                mInstanceMap.put(t,instance);
                return instance;
            }
        }
        
        public static void clear(){
            final Iterator<Map.Entry<Class, Object>> iterator = mInstanceMap.entrySet().iterator();
            while (iterator.hasNext()){
                final Map.Entry<Class, Object> next = iterator.next();
                final Object value = next.getValue();
                if (value instanceof Clearable) {
                    ((Clearable) value).clear();
                }
            }
        }
    }
    

    ​ 首先定义一个clearable接口,里面只有一个clear方法。然后所有需要清数据的单例都去实现这个接口(有些单例与帐号无关,不需要做清理),做自己的清理操作,这样的话,我们在清理数据的时候只需要去调Instance.clear()方法,它会遍历所有的单例,如果实现了clearable接口的话,就去调它的clear方法。做到这些还不够,因为我们没有对单例类做限制,随便丢一个阿猫阿狗类进来都会丢出去一个对象,这样做很容易被滥用,所以我们还得做些限制:

    public interface SingleInstantiatable {
    }
    
    public interface Clearable extends SingleInstantiatable{
        void clear();
    }
    
    public class SingleInstance {
        private static ConcurrentHashMap<Class<? extends SingleInstantiatable>,Object> mInstanceMap = new ConcurrentHashMap<>();
        public static  <T extends SingleInstantiatable> T get(Class<T> t){
            Object o = mInstanceMap.get(t);
            if (o != null) {
                return (T) o;
            }
            synchronized (t) {
                o = mInstanceMap.get(t);
                if (o != null) {
                    return (T) o;
                }
                T instance = IMReflectUtils.createNoArgumentInstanceFromClass(t);
                mInstanceMap.put(t,instance);
                return instance;
            }
        }
    
        public static void clear(){
            for (Map.Entry<Class<? extends SingleInstantiatable>, Object> next : mInstanceMap.entrySet()) {
                //遍历现有的所有单例,该清清,该丢丢
                final Object value = next.getValue();
                if (value instanceof Clearable) {
                    ((Clearable) value).clear();
                }
            }
        }
    }
    

    ​ 像上面这样,先定义一个SingleInstantiatable接口,它没有任何实现,目的只是为了对单例类做限制,所有的单例类都要实现这样接口,然后让Clearable去继承这个接口。现在SingleInstance.get()方法就只接受实现了SingleInstantiatable接口的类了,可以最大限度地避免被滥用。

    ​ 到了这里,这个类基本上是完成了,我们先看一下它有哪些好处:

    1.所有的单例类都不需要去关心自身的创建问题,省掉了线程同步的代码,避免创建出有问题的单例

    2.所有的单例类都只要实现一个clear()方法(在有需要的情况下),而不必担心它不会被调用

    3.省掉了开发者挨个去清理数据的代码

    ​ 再看一下它有哪些问题:

    1.开发者在使用单例的时候需要多认识一个类:SingleInstance

    2.在实现单例的时候需要实现一个空的接口:SingleInstantiatable

    3.写起来会比通常用的单例写法麻烦一点:现在用的是SingleInstance.get(Singleton.class).doSomething(),而传统的是Singleton.getInstance().doSomething()——这个其实不是问题,和上面的第一个问题一样,我们可以在Singleton.getInstance()方法里返回SingleInstance.get(Singleton.class),这样就跟传统的写法一样,要用到这个单例的地方用Singleton.getInstance()即可。

    4.创建单例是通过反射创建,不带参数(当然要带也没问题),不支持带 参数的单例——关于这一点,其实当一个单例需要参数去创建的时候,就很蛋疼了,因为你在任何地方都需要知道这个参数是什么。

    ​ 再考虑一下多线程的场景:首先get方法本身是线程安全的,如果它还不安全,就让它变得安全。那么唯一有可能出问题的就是一个线程get另一个线程clear了。

    1.有没有可能A线程把某个单例的数据clear掉了以后,B线程去get,然后又往里面塞数据呢?有!

    2.有没有可能A线程正在clear的时候,B线程又往里面塞了个单例写了数据,然后这个数据就被清掉了呢?有!

    ​ 上面这些问题可以统称为切换帐号带来的脏数据问题,但是它们并不是这种写法引入的,传统的写法同样避免不了这些问题。所以我们还是只需要考虑上面列出来的那些问题就好 了。

    题外话:切换帐号的脏数据问题

    ​ 至于切换帐号的脏数据问题,目前看来没有什么切实有效的解决方案,因为我们没有办法在切帐号的时候把所有的后台线程都同时停掉,而在继续跑的这些线程里,总可能存在一个或者几个正在进行IO请求,当这些请求成功返回后,如果有单例对其进行处理,那么这些脏数据就有可能被存下来了,最坏的情况是这些脏数据被持久化了。

    ​ 之前IM这边有过一段清理缓存的代码, 它会被执行两次,一次是帐号登出后,一次是帐号登录成功后,其原因就在于,退出当前帐号执行的那一次可能清不干净,所以为了避免出BUG,干脆不管三七二十一,登录成功了再清一次就是了。这种做法当然也没什么问题,虽然锉了一点,但至少是有效的,直到遇上工厂的懒加载……后来Y总提出一个解决方案:把请求数据和处理数据分开写,请求数据写在observable里,数据处理写在subscriber里,把subscriber保存成成员变量,然后在切换帐号的时候把这些subscriber反注册掉,这样子哪怕切换帐号后某些线程还在跑,请求还在调,由于它的subscriber已经被反注册了,这些脏数据就没有可处理的地方了。这种做法必须是OK的,但是呢,还是有一点小小的瑕疵:

    ​ 1.它没办法解决在某个临界点subsriber被反注册后又被另一个线程挂上去的问题

    ​ 2.它必须对IO请求做改造,改成上述模式

    ​ 3.它必须知道哪些类里面保存了这些subscriber,并且在切换帐号的时候去反注册它

    ​ 那么如果我们像上面那样,把单例集中起来管理的话,有没有可能解决这些问题呢?目前所能想到的,只能解决一部分,还不能完全搞定。思路是我们对SingleInstance类的缓存再做一点改造,为每个缓存起来的单例增加一个uid(或者其它什么标识,只要能区分用户的不同身份即可),代码比较长,放在最下面,有兴趣的可以看一下。这种方案为脏数据提供了单独的实例,保证脏数据不会进入缓存,但是它不能解决的恰恰是最坏的那种情况:脏数据被持久化了,因为它没有对数据的持久化做任何的限制。

    ​ 鉴于此,又有了另一个思路,是不是可以把IO请求统一管理呢?比如在发起请求的时候记录一个uid,在请求结束之后与当前uid相比,如果不同就把数据丢掉。或者我们更凶残一点,切换完帐号直接把wifi给关了再开?

    public class SingleInstance {
        private static ConcurrentHashMap<Class<? extends SingleInstantiatable>,InstanceHolder> mInstanceMap = new ConcurrentHashMap<>();
        public static  <T extends SingleInstantiatable> T get(Class<T> t){
            long beforeUid = getUid();
            if (beforeUid == 0){
                //uid为0,说明用户已经退出,直接生成一个新的实例丢出去给脏数据
                return IMReflectUtils.createNoArgumentInstanceFromClass(t);
            }
            InstanceHolder o = mInstanceMap.get(t);
            if (o != null) {
                if (beforeUid != o.uid && o.uid != 0){
                    //uid发生变化,说明帐号被切换了
                    return IMReflectUtils.createNoArgumentInstanceFromClass(t);
                }
                return (T) o.instance;
            }
            synchronized (t) {
                long currentUid = getUid();
                if (beforeUid != currentUid){
                    //uid发生变化,说明帐号被切换了
                    return IMReflectUtils.createNoArgumentInstanceFromClass(t);
                }
                o = mInstanceMap.get(t);
                if (o != null) {
                    o.uid = currentUid;
                    return (T) o.instance;
                }
                T instance = IMReflectUtils.createNoArgumentInstanceFromClass(t);
                currentUid = getUid();
                if (beforeUid != currentUid){
                    //实例创建出来后uid变了,直接丢出不进缓存
                }
                InstanceHolder holder = new InstanceHolder();
                holder.instance = instance;
                holder.uid = getUid();
                mInstanceMap.put(t,holder);
                return instance;
            }
        }
    
        public static void clear(){
            for (Map.Entry<Class<? extends SingleInstantiatable>, InstanceHolder> next : mInstanceMap.entrySet()) {
                //遍历现有的所有单例,该清清,该丢丢
                final InstanceHolder value = next.getValue();
                if (value.instance instanceof Clearable) {
                    ((Clearable) value.instance).clear();
                }
                //把当前单例的uid置0
                value.uid = 0;
            }
        }
    
        private static long getUid(){
            return AppFactory.instance().getUid();
        }
    
        private static class InstanceHolder{
            public Object instance;
            public long uid;
        }
    }
    

    相关文章

      网友评论

          本文标题:关于单例使用的72种姿势

          本文链接:https://www.haomeiwen.com/subject/bholkqtx.html