EventBus源码学习--并发原理

一、EventBus概述

  Guava在guava-libraries中为我们提供了事件总线EventBus,总线的概念大家应该都有了解,例如esb、或者dubbo的url,这些总线可以对分布式系统进行解耦,EventBus大致思路也如此,他通过事件发布订阅模式,进行系统内部组件或业务模块之间的解耦。本文以最简单的EventBus实例,讲解EventBus并发的处理原理。

二、并发遇到的问题

  EventBus的总线机制有同步和异步(AsyncEventBus)两种,不论哪种,他们处理并发的同步原理都是一致的。如果不能很好的处理EventBus并发的问题,很可能会造成总线事件阻塞,请看以下代码:

public class EventBusMain {
    public static void main(String[] args){
        EventBus bus = new EventBus();
        bus.register(new Observer());
        bus.post(new Event1());
        bus.post(new Event2());
    }
}

class Observer{
    @Subscribe
    public void test1(Event1 event1) throws InterruptedException {
        for(int i = 0 ; i < 10 ; i++){
            System.out.println("test1 , threadId=" + Thread.currentThread().getId());
            Thread.sleep(500);
        }
    }
    @Subscribe
    public void test2(Event2 event2) throws InterruptedException {
        for(int i = 0 ; i < 10 ; i++){
            System.out.println("test2 , threadId=" + Thread.currentThread().getId());
            Thread.sleep(500);
        }
    }
}
class Event1{}
class Event2{}

  代码执行后,结果如下:
  test1 , threadId=1
  test1 , threadId=1
   …
  test1 , threadId=1
  test2 , threadId=1
  test2 , threadId=1
   …
  test2 , threadId=1

  我们看到,单线程发送的多个事件是会进行阻塞的,所以如果我们触发的方法是耗时操作,那么是一定会产生事件阻塞的。那么在多线程的场景下会是怎样情形,请看代码:

        EventBus bus = new EventBus();
        bus.register(new Observer());
        new Thread(new Runnable() {
            @Override
            public void run() {
                bus.post(new Event1());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                bus.post(new Event2());
            }
        }).start();

  运行上面代码,结果如下:
  test2 , threadId=14
  test1 , threadId=13
  test2 , threadId=14
  test1 , threadId=13
  test2 , threadId=14
  test1 , threadId=13
  test2 , threadId=14
  test1 , threadId=13
  可以看出,在多线程的情况下如果两个线程发送不同的Event事件是不会阻塞的,可以并行执行(可以看threadId),也许你会说,在实际的项目中,每一个http请求就对应到系统中的一个线程,所以每个请求彼此是不会阻塞的,如果你这样认为,那你一定会被坑,请看下面代码:

        EventBus bus = new EventBus();
        bus.register(new Observer());
        new Thread(new Runnable() {
            @Override
            public void run() {
                bus.post(new Event1());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                bus.post(new Event1());
            }
        }).start();

  代码执行后,结果如下:
  test1 , threadId=13
  test1 , threadId=13
   …
  test1 , threadId=13
  test1 , threadId=14
  test1 , threadId=14
   …
  test1 , threadId=14
  从结果看出来,即使在多线程的情况下,如果发出的是同一种Event事件,那么仍然是阻塞的,如果想解决这个问题,必须使用@AllowConcurrentEvents,请看代码:

class Observer{
    @Subscribe
    @AllowConcurrentEvents
    public void test1(Event1 event1) throws InterruptedException {
        for(int i = 0 ; i < 10 ; i++){
            System.out.println("test1 , threadId=" + Thread.currentThread().getId());
            Thread.sleep(500);
        }
    }

    @Subscribe
    @AllowConcurrentEvents
    public void test2(Event2 event2) throws InterruptedException {
        for(int i = 0 ; i < 10 ; i++){
            System.out.println("test2 , threadId=" + Thread.currentThread().getId());
            Thread.sleep(500);
        }
    }
}

  当加上这个注解后,就可以并行执行了,执行结果就不列举了。

三、源码分析

  在我们上面写的代码中,关键的几行就是:

        EventBus bus = new EventBus();
        bus.register(new Observer());
        bus.post(new Event1());

  1. 先从第一行开始,无非是EventBus的构造函数,点进去我们看到:
这里写图片描述
  我们看到,虽然调用了默认无参构造函数,但是无参构造函数后续调用了其他构造函数,最终设置了EventBus的id、任务执行器,事件调度器等等,后续会介绍这几个属性的作用。

  2.继续看第二行代码,是观察者的注册,跟进源码看到调用了SubscriberRegistry的register的方法,继续跟进去看到:
这里写图片描述
  先看register方法的第一行代码:

Multimap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener);

  这个方法的作用是找到当前EventBus所有已经注册了的观察者中的所有方法,然后封装成Multimap,什么意思呢?就是说,我们上面自己写的代码中,如果Event1这个事件,在多个Observer的多个方法中都存在,那么我们的bus.post(new Event1())时,应该吧所有的Event1的方法都通知到,所以,在EventBus中必须知道Event1对应的所有method,也就是类似[Event1, List< method > ], 这样的键值对,如果新注册的观察者中,也存在Event1的方法,那我们需要把这个method也放到map的List< method > 而Multimap就是这样一个数据格式的键值对map。我们可以点findAllSubscribers进去看下:
这里写图片描述

  从代码中看到,先创建Multimap,然后获取观察者的class,通过观察者的class,获取这个观察者所有标注了@Subscribe的方法,然后通过方法的method获取method的参数,默认认为第一个参数就是Event事件,然后把event事件的class作为key,把当前的eventbus、观察者、method封装成一个Subscriber。从Subscriber.create点进去再看下如何创建:

  其中isDeclaredThreadSafe(method)方法,就是判断当前方法是否标记了上面说的@AllowConcurrentEvents注解,如果有这个注解,那么就说明这个方法支持并行执行,获取到的是Subscriber对象,如果没有这个注解,说明这个方法只能串行执行,获取到SynchronizedSubscriber。

  再回到上面的register方法,获取到这个map之后,开始遍历这个map,把通过key(这个key就是EventType),去SubscriberRegistry的缓存中找一下这个EventType是否存在对应的method,如果存在,就是value(就是对应Subscriber集合)加入到缓存中,如果不存在,就重新创建集合在把value放入新的集合中。这样SubscriberRegistry的缓存中就把新注册的Observer的EventType和Subscriber对应关系存储好了。

  3. 再看第三行bus.post(new Event1());跟代码进去:
这里写图片描述

   先通过EventType查找到所有的Subscriber,然后迭代,如果找不到就进入DeadEvent。我们在看dispatch方法,是PerThreadQueuedDispatcher的dispatch方法:

这里写图片描述

   略过checkNotNull方法,看到queue.get,其中queue是一个ThreadLocal,他为每一个thread创建了一个队列,我们把当前的Subscribers和event封装成Event然后在放入队列,这样做的目的是在使用AsyncEventBus时,同一个线程可以并行触发多个event事件,但是这里还是把并行的事件放入队列串行,只是在消费的时候并行执行,使用EventBus时可以不用考虑这里的逻辑,不管是否使用都不影响。

   再向后看if (!dispatching.get()),这里的dispatching也是个threadLocal,他为每一个thread存储了一个boolean,只有当boolean为false的时候,才会进行,一旦进行,那么这个boolean就设置为true,防止这些Subscribers被重复同一个线程执行。

   在向下就是对Subscribers集合的迭代,取出每一个Subscriber,然后调用Subscriber的dispatch方法,我们知道,刚才我们在创建Subscriber时,时根据方法是否标记@AllowConcurrentEvents来创建的,有@AllowConcurrentEvents的方法创建的是Subscriber,没有的创建的SynchronizedSubscriber,二者的dispatch大家可以看下:
   这是Subscriber的:
这里写图片描述

   我们看到他被executor执行,executor是最开始说的EventBus构造方法中设置的默认executor,他的execute方法的实现很简单,大家可以自己点进去看这里不赘述,这个executor没有做同步的控制,例如synchronized或者lock,也就是说可以并行执行的。

   这是SynchronizedSubscriber的方法:
这里写图片描述
  我们发现这里增加了synchronized(this),当多个线程提交同一个event时,他们会触发同一个method,也就是这里的同一个SynchronizedSubscriber对象,同一个对象会被synchronized(this)的锁阻塞,因此就造成了最开始代码中说的阻塞效果。

  以上就是对eventbus并发处理的原理说明,笔者能力有限,有问题之处望指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值