写在前面
相信绝大多数Java后端同学对于Guava这个强大的开源工具都不陌生,一些喜欢研读源码的同学甚至可能对于Guava源码也相当熟悉了。最近由于项目所需,重读了Guava部分源码,对于Guava中某些设计或者说实现有了不一样的认识和理解,今天分享一下Guava同步事件默认调度器PerThreadQueuedDispatcher。
源码
节省篇幅,仅贴部分重点源码:
private final ThreadLocal<Queue<Event>> queue =
new ThreadLocal<Queue<Event>>() {
@Override
protected Queue<Event> initialValue() {
return Queues.newArrayDeque();
}
};
private final ThreadLocal<Boolean> dispatching =
new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return false;
}
};
@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();
}
}
}
解读
Guava事件总线支持同步和异步事件,事件发布之后会交由调度器做分发,而同步事件默认的事件调度分发器就是PerThreadQueuedDispatcher,源码的注释告诉我们这个调度器匹配的是原始的分发调度行为。具体的实现方式如上面源码所示,定义了两个threadLocal变量,一个存放事件队列,一个存放是否调度中的状态,具体作用及实现暂时按下不表。
我们先假设一个场景:发布事件A,订阅者收到后执行并发布事件B,事件B的订阅者收到后执行并发布事件C……以此类推。
现在我们思考一个问题,如果我们自己实现一个单线程事件总线去支持上述场景功能该怎么实现呢?最初的答案可能很简单,简化三步骤,同步发布事件->订阅者接收事件->处理事件。这个实现确实简单明了,但是要处理上面的场景会有一个致命的问题,直接递归导致处理事件调用栈可能会很深,只有最后一个事件处理完成后整个调用才会回调。如何优化解决调用栈过深的问题呢?我们可以想到中间加一个事件等待队列,发布事件到同步触发去执行的逻辑修改为将事件加入队列以后就直接返回,但是这个时候事件的真正执行就得有一个额外的线程去轮询队列、取事件去执行,这样的实现显然也不好,且不是真正意义上的单线程。那应该怎么实现好呢?书接上回我们来看看Guava的实现。
前面提到Guava的PerThreadQueuedDispatcher中定义了两个threadLocal变量,一个存放事件队列,一个存放是否调度中的状态。队列的作用显而易见,就是为了实现前面提到的匹配原始调度行为,也就是单线程先进先出,先到的先执行。而是否调度中状态值dispatching,源码注释中给我们的解释是,防止重入事件调度。重入指的就是发布了事件A,订阅者收到后执行并发布事件B这类场景。还是前面提到的那个场景,当前线程中调度器开始处理事件A,把事件A放入队列,然后设置dispatching为true,dispatchEvent的时候发布了事件B,事件B在调度的时候也进入队列,这个时候当前线程的dispatching值为true,事件B的dispatch直接“结束”了,也就是说尽管事件B是在执行事件A的时候发布的,但是此时在这个线程中事件B的真正执行对于事件A已经没有影响了,事件B的真正执行是在后续循环中处理的,以此类推。
总结一下,其实Guava单线程事件调度器使用了一个队列+一个调度状态,将递归调用转为在当前线程上的循环调用,在满足了匹配原始分发调度行为需求的同时,完美解决了调用栈过深的问题,不可谓不精妙。