google guava库的事件总线组件EventBus,是观察者模式(进程内事件发布/订阅)的一个比较优雅的实现。之前项目一直没什么机会使用(之前事件解耦多用消息中间件),最近自个儿摸索DDD,在进程内领域事件的实现时,就采用了EventBus。总体使用比较顺利,其提供的api也比较简单,在这里就不详诉使用方式了。今天就如标题所说,谈谈EventBus post事件时的阻塞行为。
具体遇到的令我困惑的行为是,在不同线程里post事件,貌似也会相互阻塞。以下是实验代码:
public static void main(String[] args) throws InterruptedException {
EventBus eventBus = new EventBus();
eventBus.register(new Object(){
@Subscribe
public void onPublish(Object event) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
System.out.println("事件处理了,thread id is " + Thread.currentThread().getId());
}
});
// 新开一个线程post事件
new Thread(() -> {
eventBus.post(new Object());
System.out.println("事件发布了---1111");
}).start();
eventBus.post(new Object());
System.out.println("事件发布了---2222");
TimeUnit.HOURS.sleep(1);
}
以上代码中,我新开了一个线程去post事件,按照我原先的设想,事件订阅代码应该是差不多同时执行,但是事实是在一个事件post完5秒后另一个post方法才返回成功。难道是post(Object event)
这个方法是相互阻塞的?同一时间只能post一个event,不能并发post?
带着问题翻源码,发现post的调度并没有阻塞的地方,以下是post源码:
public void post(Object event) {
Iterator<Subscriber> eventSubscribers = subscribers.getSubscribers(event);
if (eventSubscribers.hasNext()) {
dispatcher.dispatch(event, eventSubscribers);
} else if (!(event instanceof DeadEvent)) {
// the event had no subscribers and was not itself a DeadEvent
post(new DeadEvent(this, event));
}
}
主要委托给dispatcher的dispatch调用,EventBus使用的dispatcher是PerThreadQueuedDispatcher
,它的特点是内部的事件队列使用了ThreadLocal,线程之间的事件是相互隔离的,所以不存在阻塞
/** Per-thread queue of events to dispatch. */
private final ThreadLocal<Queue<Event>> queue =
new ThreadLocal<Queue<Event>>() {
@Override
protected Queue<Event> initialValue() {
return Queues.newArrayDeque();
}
};
继续往下看PerThreadQueuedDispatcher.dispatch
:
@Override
void dispatch(Object event, Iterator<Subscriber> subscribers) {
checkNotNull(event);
checkNotNull(subscribers);
Queue<Event> queueForThread = queue.get();
queueForThread.offer(new Event(event, subscribers));
if (!dispatching.get()) {
dispatching.set(true);
try {
Event nextEvent;
while ((nextEvent = queueForThread.poll()) != null) {
while (nextEvent.subscribers.hasNext()) {
nextEvent.subscribers.next().dispatchEvent(nextEvent.event);
}
}
} finally {
dispatching.remove();
queue.remove();
}
}
}
总体逻辑就是event入队(queue是ThradLocal变量,线程之间隔离,不会阻塞),然后从队列取任务分发执行,dispatching也是一个ThradLocal变量,应该是为了防止listener的逻辑中重复发布相同事件导致死循环。看到这里还是看不出阻塞在哪里,继续往下看
/** Dispatches {@code event} to this subscriber using the proper executor. */
final void dispatchEvent(final Object event) {
executor.execute(
new Runnable() {
@Override
public void run() {
try {
invokeSubscriberMethod(event);
} catch (InvocationTargetException e) {
bus.handleSubscriberException(e.getCause(), context(event));
}
}
});
}
这里的exector就是DirectExecutor
,在当前线程下执行:
@GwtCompatible
enum DirectExecutor implements Executor {
INSTANCE;
@Override
public void execute(Runnable command) {
command.run();
}
@Override
public String toString() {
return "MoreExecutors.directExecutor()";
}
}
看来真正阻塞的地方是在invokeSubscriberMethod(event);
这个方法里了,
继续往下,发现SubScriber有两个实现,Subscriber
和SynchronizedSubscriber
,他们都是在SubScriber的静态方法create()中使用的
/** Creates a {@code Subscriber} for {@code method} on {@code listener}. */
static Subscriber create(EventBus bus, Object listener, Method method) {
return isDeclaredThreadSafe(method)
? new Subscriber(bus, listener, method)
: new SynchronizedSubscriber(bus, listener, method);
}
isDeclaredThreadSafe方法是判断注册的监听方法有没有AllowConcurrentEvents
这个注解
private static boolean isDeclaredThreadSafe(Method method) {
return method.getAnnotation(AllowConcurrentEvents.class) != null;
}
如果有就创建Subscriber 否则 创建SynchronizedSubscriber,后者相比前者,区别就是调用目标方法是使用了synchronize关键字,从而达到了阻塞的效果
@Override
void invokeSubscriberMethod(Object event) throws InvocationTargetException {
synchronized (this) {
super.invokeSubscriberMethod(event);
}
}
至此,问题答案终于知晓,post方法之所以被阻塞,不是生产者发布事件时阻塞而是消费者消费事件时使用了synchronize关键字实现了同步消费的效果,回到文章开始的实验,只要消费方法添加AllowConcurrentEvents
注解,就不会出现阻塞的效果了,实现了并发消费。
@Subscribe
@AllowConcurrentEvents
public void onPublish(Object event) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
System.out.println("事件处理了,thread id is " + Thread.currentThread().getId());
}