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, 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并发处理的原理说明,笔者能力有限,有问题之处望指正。