Android之EventBus框架源码解析上(单例模式)

转载请标明出处:【顾林海的博客】

个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持!
在这里插入图片描述

前言

EventBus能够简化各组件间的通信,让我们的代码书写变得简单,能有效的分离事件发送方和接收方(也就是解耦的意思),能避免复杂和容易出错的依赖性和生命周期问题。关于它的使用方式,同学们可以查看相关文章。

EventBus.getDefault().register(this);

以上是EventBus的注册,很简单,通过getDefault()方法获取EventBus实例,再通过它的register(Object subscriber)方法注册事件接受的类。

我们为什么要去看EventBus的源码呢?一是为了了解它的实现原理,二是学习作者的代码编写时的思想包括用到的设计模式,当然更重要的是学习到大神们的编程思维(知识),进而提升自己的代码水平(工资)。从getDefault()方法开始,代码如下:

public class EventBus {

    static volatile EventBus defaultInstance;
    
    public static EventBus getDefault() {
        if (defaultInstance == null) {
            synchronized (EventBus.class) {
                if (defaultInstance == null) {
                    defaultInstance = new EventBus();
                }
            }
        }
        return defaultInstance;
    }

}

看到上面的代码-单例,相信大家在很多开源项目中都用到过,比如图片加载框架Picasso和Glide,为什么以上这种创建对象的方式在一些著名的开源框架中被运用,这是我们关注的,
就像上面的代码中,为什么这么写?首先要了解DCL(双重检查锁定)和volatile的相关概念。

DCL(双重检查锁定)

相对于我们前端开发中,很少,甚至都接触不到多线程相关的知识,DCL(双重检查锁定)就是在多线程中,用于延迟初始化来降低初始化类和创建类的开销。在创建对象时,如果对象初始化操作
需要高开销,这时会采用一些延迟化初始化方案,比如DCL(双重检查锁定),比如,下面就是一个非线程安全的延迟初始化对象的实例。

public class Demo {
    private static Demo mDemo;

    public static Demo getInstance() {
        if (null == mDemo) {//1
            mDemo = new Demo();//2
        }
        return mDemo;
    }
}

其实上面代码并没有什么问题(单线程),但如果在多线程的情况下,就有可能出现问题,我们来分析下问题出在哪里,假设当前有两个线程A和B,并且这两个线程都执行了上面
这段代码,如果A线程执行到代码1,B线程也执行到代码1,此时mDemo引用的对象都为空,接下来B线程执行代码2,对象创建完毕,这时回过头看线程A,由于之前线程
A在判断mDemo引用的对象为空,线程A就会执行到代码2,可以发现mDemo对象创建了两次,造成额外的开销。

如何去解决这个问题呢,可以对这个方法进行同步处理,使用synchronized。代码如下:

public class Demo {
    private static Demo mDemo;

    public synchronized static Demo getInstance() {
        if (null == mDemo) {//1
            mDemo = new Demo();//2
        }
        return mDemo;
    }
}

使用这种方式在多线程频繁调用的情况下,会导致程序性能的下降,此时DCL(双重检查锁定)登场,代码如下:

public class Demo {
    private static Demo mDemo;

    public static Demo getInstance() {
        if (null == mDemo) {//1
            synchronized (Demo.class) {//2
                if (null == mDemo) {//3
                    mDemo = new Demo();//4
                }
            }

        }
        return mDemo;
    }
}

通过DCL可以大幅降低synchronized带来的性能开销,在多线程中试图在同一时刻创建对象时,会通过加锁来保证只有一个线程能创建对象,同时在对象创建
完毕后,执行getInstance()方法不需要获取锁,直接返回已创建好的对象。
使用DCL看起来很完美,但遇到重排序的问题时,就会出现错误,比如在代码1处的mDemo不为null时,mDemo引用的对象有可能还没有完成初始化。

重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。编译器和处理器在做重排序时,会遵守数据依赖性,也就是说不会改变
存在数据依赖关系的两个操作的执行顺序,数据依赖性仅仅针对单个处理器中执行的指令序列和单个线程中执行的操作。

回过头看上面代码中的代码4处可以分解为3步:

  • 第一步:分配对象的内存空间
  • 第二步:初始化对象
  • 第三步:设置mDemo指向刚分配的内存地址

当遇到重排序的问题是,第二步和第三步就会重排序,重排序后的顺序如下:

  • 第一步:分配对象的内存空间
  • 第三步:设置mDemo指向刚分配的内存地址
  • 第二步:初始化对象

第二步和第三步重排序并不会改变单线程内的程序执行结果,这里还是以两个线程A和B为例,它们的时间线如下:

  • A1:分配对象的内存空间
  • A3:设置mDemo指向刚分配的内存地址
  • B1:判断mDemo是否为空
  • B2:mDemo不为空,线程B访问mDemo引用的对象
  • A2:初始化对象
  • A4:访问mDemo引用的对象

从线程A和B的时间线中可以看出,执行线程B时,由于线程A进行了重排序,导致mDemo在没有初始化对象时就已经分配了内存地址时,从而线程B执行代码1处
发现mDemo不为空,从而获取了一个未初始化的对象。

volatile

既然DCL的问题已经出现了,那我们总归要解决它,给出的方案是基于volatile的解决方案(JDK5或更高版本)。只需要修改代码如下:

public class Demo {
    private volatile static Demo mDemo;

    public static Demo getInstance() {
        if (null == mDemo) {//1
            synchronized (Demo.class) {//2
                if (null == mDemo) {//3
                    mDemo = new Demo();//4
                }
            }

        }
        return mDemo;
    }
}

当申明的对象为volatile时,在多线程中重排序会被禁止,从而解决了DCL带来的重排序问题。

volatile的主要作用是使变量在多个线程间可见,首先我们要明白在在线程中创建的变量会被存放在两个堆栈中,分别是:

  • 公共堆栈
  • 线程的私有堆栈

当我们在主线程中设置某个线程中的变量,该线程中的变量会从私有堆栈中获取,而主线中给该线程设置的值被更新到公共堆栈中,这样的话会导致私有
堆栈和公共堆栈数据不同步的问题,通过使用volatile关键字,可以强制的从公共内存中读取变量,从而保持线程间访问某个变量时数据同步,volatile
增加了实例变量在多个线程间的可见性。

volatile的一个比较明显的缺点是不支持原子性,它解决的是变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程间访问资源的同步性。关于
volatile就介绍到这里,感兴趣的同学可以阅读Java并发编程的艺术这本书。

©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:上身试试 返回首页