Java最新聊一聊 EventBus 源码和设计之禅,腾讯,字节等大厂面试真题汇总

写在最后

作为一名即将求职的程序员,面对一个可能跟近些年非常不同的 2019 年,你的就业机会和风口会出现在哪里?在这种新环境下,工作应该选择大厂还是小公司?已有几年工作经验的老兵,又应该如何保持和提升自身竞争力,转被动为主动?

就目前大环境来看,跳槽成功的难度比往年高很多。一个明显的感受:今年的面试,无论一面还是二面,都很考验Java程序员的技术功底。

最近我整理了一份复习用的面试题及面试高频的考点题及技术点梳理成一份“Java经典面试问题(含答案解析).pdf和一份网上搜集的“Java程序员面试笔试真题库.pdf”(实际上比预期多花了不少精力),包含分布式架构、高可扩展、高性能、高并发、Jvm性能调优、Spring,MyBatis,Nginx源码分析,Redis,ActiveMQ、Mycat、Netty、Kafka、Mysql、Zookeeper、Tomcat、Docker、Dubbo、Nginx等多个知识点高级进阶干货!

由于篇幅有限,为了方便大家观看,这里以图片的形式给大家展示部分的目录和答案截图!

Java经典面试问题(含答案解析)

阿里巴巴技术笔试心得

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

for (Method method : methods) {
int modifiers = method.getModifiers();
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length == 1) { Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class); if (subscribeAnnotation != null) { Class<?> eventType = parameterTypes[0];
// needCheck
if (findState.checkAdd(method, eventType)) {
ThreadMode threadMode = subscribeAnnotation.threadMode();
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
}
}
}
}
}
}

这里想要提及的一点事,获取到 @Subscribe 修饰的目标方法后,并非无脑地添入 subscriberMethods 中,而实际上是需要过滤一遍的,讲解 checkAdd() 源码前,希望读者思考以下几个问题:

  • 对于同一个 Event,当前类对该对象使用了多个方法进行了多次订阅,那么如果该 Event 被发射的时候,当前类会如何调用这些方法?
  • 对于同一个 Event,父类对该对象进行了一次订阅,子类重写该订阅方法,那么如果该 Event 被发射的时候,父类子类当中会如何处理这些方法?

解决这些方法就需要去看看 checkAdd() 的底层实现了——

boolean checkAdd(Method method, Class<?> eventType) {
Object existing = anyMethodByEventType.put(eventType, method);
if (existing == null) {
return true;
} else {
return checkAddWithMethodSignature(method, eventType);
}
}

可以看到 anyMethodByEventType 使用了 Event 的 Class 作为键,这像是意味着一个类对于同一个 Event 只能订阅一次,事实上是不是这样,还得继续看看 checkAddWithMethodSignature(),其源码简化如下:

private boolean checkAddWithMethodSignature(Method method, Class<?> eventType) {
methodKeyBuilder.setLength(0);
methodKeyBuilder.append(method.getName());
methodKeyBuilder.append(‘>’).append(eventType.getName());

String methodKey = methodKeyBuilder.toString();
Class<?> methodClass = method.getDeclaringClass(); Class<?> methodClassOld = subscriberClassByMethodKey.put(methodKey, methodClass);
if (methodClassOld == null || methodClassOld.isAssignableFrom(methodClass)) {
return true;
} else {
subscriberClassByMethodKey.put(methodKey, methodClassOld);
return false;
}
}

可以看到 subscriberClassByMethodKey 使用方法名 + '>' + 事件类型作为键,这意味着对于同一个类来说,subscriberClassByMethodKey 肯定不会键重复(毕竟一个类中不能够方法名相同且方法参数、个数都相同),因此它最终会返回 true。这意味着一个类如果使用了多个方法对同一个 Event 对象进行注册,那么当该 Event 对象被发射出来的时候,所有的方法都将会得到回调。

但是当父类执行上述操作的时候,如果子类有「显示」实现父类的订阅方法,那么此时 subscriberClassByMethodKey.put(methodKey, methodClass) 返回值不会为空,且为子类的 Class,此时 if 上分支将会判断子类 Class 是否 isAssignableFrom 父类 Class,这肯定是会为 false 的,这将会走入 if 下分支并返回 false。这意味着当子类「显示」实现父类的订阅方法的时候,如果此时发射指定 Event 的话,父类的订阅方法将不会执行,而仅会执行子类的订阅方法。

subscribe()

获取到相应的 SubscriberMethod 链表后,就是对链表中的 SubscriberMethod 对象进行订阅了,EventBus#subscribe() 方法源码精简如下:

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
Class<?> eventType = subscriberMethod.eventType;
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
CopyOnWriteArrayList subscriptions = subscriptionsByEventType.get(eventType);
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
}

int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
// 根据 priority 大小放入 List 中
subscriptions.add(i, newSubscription);
break;
}
}

List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}
subscribedEvents.add(eventType);

// 省略 sticky 事件
}

subscriptionsByEventType 根据 Event 事件类类型获取订阅信息链表,当然,如果没有的话那就 new 一个并放入其中。接着根据订阅方法的优先级塞入该链表中。最后 typesBySubscriber 获取该 subsciber 的所有 Event 事件类型链表,并添加当前 Event 事件类型。关于 sticky 事件的具体内容在 sticky 中会具体讲解。

至此 EventBus#register(Object) 方法算是结束了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

post()

EventBus#post(Object) 源码精简如下:

public void post(Object event) {
PostingThreadState postingState = currentPostingThreadState.get();
List eventQueue = postingState.eventQueue;
eventQueue.add(event);

// 确保不会被调用多次
if (!postingState.isPosting) {
postingState.isMainThread = isMainThread();
postingState.isPosting = true;
try {
while (!eventQueue.isEmpty()) {
// 分发 Event 事件
postSingleEvent(eventQueue.remove(0), postingState);
}
} finally {
// 最后要 reset flag
postingState.isPosting = false;
postingState.isMainThread = false;
}
}
}

currentPostingThreadState 是一个 ThreadLocal 类,通过它获取到 PostingThreadState 对象,再根据该对象获取到 event 链表(有没有联想到 Android 中的消息机制?),并将传入的 event 塞入该链表。为了控制 Event 出队列不会被调用多次,PostingThreadState 对象有一个 isPosting 来标记当前链表是否已经开始进行回调操作,通过源码可以看到,每次分发完一个 Event 事件,该事件也会被从链表中 remove 出去。

postSingleEvent()

具体 postSingleEvent() 源码精简如下:

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
Class<?> eventClass = event.getClass();
postSingleEventForEventType(event, postingState, eventClass);
}

追溯 EventBus#postSingleEventForEventType() 源码精简如下:

private void postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
CopyOnWriteArrayList subscriptions;
synchronized (this) {
subscriptions = subscriptionsByEventType.get(eventClass);
}
if (subscriptions != null && !subscriptions.isEmpty()) {
for (Subscription subscription : subscriptions) {
postingState.event = event;
postingState.subscription = subscription;
try {
postToSubscription(subscription, event, postingState.isMainThread);
} finally {
postingState.event = null;
postingState.subscription = null;
postingState.canceled = false;
}
}
}
}

通过 subscriptionsByEventType 获取该 Event 事件对应的订阅信息链表,然后将该订阅信息Event 和当前线程信息传给了 postToSubscription() 方法,该方法戳进去一看就知道是用来去回调所有订阅方法的,该方法的具体分析在 threadMode 中。实际上到这里 post() 流程就算是结束了。所以实际上核心方法 post() 的源码是十分简单的,也可以看得到,核心字段也仅有 subscriptionsByEventType 一个而已。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

unregister()

EventBus#unregister(Object) 方法源码精简如下:

public synchronized void unregister(Object subscriber) {
List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber); if (subscribedTypes != null) { for (Class<?> eventType : subscribedTypes) {
unsubscribeByEventType(subscriber, eventType);
}
typesBySubscriber.remove(subscriber);
}
}

整体看来分两步走,一步是移除注册对象和其所有 Event 事件链表,即 typesBySubscriber 移除相关键值对的;再就是在 unsubscribeByEventType() 方法中对 subscriptionsByEventType 移除了该 subscriber 的所有订阅信息(可以看到实际上没有对 METHOD_CACHE 进行相关移除操作,便于下一次注册的时候可以很方便拿到之前的信息,这便是缓存的作用所在)。

threadMode

在 EventBus 中,共有四种 threadMode,如下:

public enum ThreadMode {
POSTING,

MAIN,

MAIN_ORDERED,

BACKGROUND,

ASYNC
}

  • POSTING:接收事件方法应执行在发射事件方法所在的线程(由于发射事件方法线程可能是主线程,这意味着接收方法不能执行耗时操作,否则会阻塞主线程)
  • MAIN:在 Android 中则接收事件方法应执行在主线程,否则(在 Java 项目中)等同于 POSTING。如果发射事件方法已位于主线程,那么接收事件方法会被「立即」调用(这意味着接收事件方法不能执行耗时操作,否则会阻塞主线程;同时,由于是「立即」调用,所以发射事件方法此时是会被接收事件方法所阻塞的),否则等同于 MAIN_ORDERED
  • MAIN_ORDERED:在 Android 中则接收事件方法会被扔进 MessageQueue 中等待执行(这意味着发射事件方法是不会被阻塞的),否则(在 Java 项目中)等同于 POSTING
  • BACKGROUND
  • 在 Android 中
  • 发射事件方法在主线程中执行,则接收事件方法应执行在子线程执行,但该子线程是 EventBus 维护的单一子线程,所以为了避免影响到其他接收事件方法的执行,该方法不应太耗时避免该子线程阻塞。
  • 发射事件方法在子线程中执行,则接收事件方法应执行在发射事件方法所在的线程。
  • 在 Java 项目中,接收事件方法会始终执行在 EventBus 维护的单一子线程中。
  • ASYNC:接收方法应执行在不同于发射事件方法所在的另一个线程。常用于耗时操作,例如网络访问。当然,尽量避免在同一个时间大量触发此类型方法,尽管 EventBus 为此专门创建了线程池来管理回收利用这些线程。

关于以上 threadMode 哪几种应避免耗时操作,耗时时阻塞的是哪条线程,希望各位读者能够仔细阅读。

说完几种 threadMode 之后,再来看看前文遗留下来的问题——postToSubscription() 源码如下:

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case MAIN_ORDERED:
if (mainThreadPoster != null) {
mainThreadPoster.enqueue(subscription, event);
} else {
// temporary: technically not correct as poster not decoupled from subscriber
invokeSubscriber(subscription, event);
}
break;
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}

细看源码,其实可以发现只用到了两种方法,一种是 invokeSubscriber 意味着立即调用该方法,另一种是 xxxPoster.enqueue() 意味着需要使用其他线程来执行该方法。

invokeSubscriber()

源码如下:

void invokeSubscriber(Subscription subscription, Object event) {
try {
//纯反射
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
} catch (InvocationTargetException e) {
handleSubscriberException(subscription, event, e.getCause());
} catch (IllegalAccessException e) {
throw new IllegalStateException(“Unexpected exception”, e);
}
}

实在是简单粗暴直接通俗易懂,笔者佩服。

那么那些情况会使用 invokeSubscriber() 方法呢?

  • POSTING:不用说,既然和发射事件线程同一条线程执行,那么当然直接调用 invokeSubscriber() 即可。
  • MAIN:在确保发射事件线程是主线程的情况下,直接调用 invokeSubscriber()
  • MAIN_ORDERED:如果当前项目不是 Android 项目情况下(纯 Java 项目),将会直接调用 invokeSubscriber()
  • BACKGROUND:前面提到如果发射事件线程不是主线程的话,接收事件将会执行于发射事件所在的线程,所以也会直接调用 invokeSubscriber()

文中已多次提到 Android 项目和纯 Java 项目,是由于在 Java 项目中大部分情况下不需要特地区分主线程和子线程(这一点笔者也得到了女票的证实)。其实不仅是 EventBus,RxJava 也是如此,RxJava 中是没有 Schedulers.mainThread() 一说的,仅有 Schedulers.trampoline() 表当前线程。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Poster#enqueue()

根据源码可以看出来分为以下三种:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

HandlerPoster 源码不在此扩展了,熟悉 Android 的读者们应该都猜得到 HandlerPoster 底层实现肯定是通过 Handler 机制来实现的,HandlerPoster#enqueue() 方法的实现离不开 Hanlder#sendMessage(),而处理方式肯定就是在 Hanlder#handleMessage() 中去调用 invokeSubscriber()

BackgroundPoster 源码也不在此扩展了,前面提到 EventBus 会维护单一线程去执行接收事件方法,所以肯定会在 Runnable#run() 中去调用 invokeSubscriber()

AsyncPoster 的底层实现实际上与 BackgroundPoster 大同小异,但是有读者会疑惑了,BackgroundPoster 底层维护的是「单一」线程,而 AsyncPoster 肯定不是这样的啊。这里的细节留到设计技巧一节再来细说。

sticky

总结

我们总是喜欢瞻仰大厂的大神们,但实际上大神也不过凡人,与菜鸟程序员相比,也就多花了几分心思,如果你再不努力,差距也只会越来越大。实际上,作为程序员,丰富自己的知识储备,提升自己的知识深度和广度是很有必要的。

Mybatis源码解析

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

自己的知识深度和广度是很有必要的。

Mybatis源码解析

[外链图片转存中…(img-ZKtdxh7b-1715442339646)]

[外链图片转存中…(img-pHezfjFT-1715442339647)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 30
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值