第一轮面试 面试官:首先问你几个基础问题。Java 中 ArrayList 和 LinkedList 的区别是什么? 王铁牛:ArrayList 是基于数组实现的,随机访问快,增删慢,尤其是在中间位置增删。LinkedList 是基于链表实现的,增删快,随机访问慢。 面试官:回答得不错。那 HashMap 在 JDK1.7 和 JDK1.8 中有什么主要区别? 王铁牛:1.8 中引入了红黑树,当链表长度大于 8 且数组长度大于 64 时,链表会转化为红黑树,提高查找效率。1.7 只有链表。 面试官:很好。接着,Spring 框架中 IOC 和 AOP 分别是什么? 王铁牛:IOC 是控制反转,把对象创建和管理的控制权交给 Spring 容器。AOP 是面向切面编程,在不修改原有代码的基础上,对业务进行增强。
第二轮面试 面试官:现在问些稍难的。多线程中线程池的工作原理是什么? 王铁牛:嗯……线程池就是有一堆线程,任务来了就分配线程去执行,满了就放到队列里,队列满了就按照拒绝策略处理。 面试官:那 JUC 包下的 CountDownLatch 是做什么的? 王铁牛:好像是……让线程等待其他线程完成一些操作,然后再继续执行。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 能防止 SQL 注入。
第三轮面试 面试官:最后一轮,问几个更深入的。Dubbo 的服务暴露和引用过程是怎样的? 王铁牛:呃……就是服务提供者把服务暴露出去,服务消费者引用,具体过程有点复杂,我不太记得清了。 面试官:RabbitMQ 中的死信队列是怎么回事? 王铁牛:死信队列就是消息变成死信后会被发送到的队列,至于为啥变成死信,好像是过期或者队列满了之类的。 面试官:xxl - job 调度中心的核心原理是什么? 王铁牛:这个……大概是能调度任务,具体原理不太清楚。
面试官:好的,今天的面试就到这里。你回去等通知吧,我们会综合评估所有候选人后,再决定是否录用你。感谢你今天来参加面试。
问题答案:
- ArrayList 和 LinkedList 的区别:
- 数据结构:ArrayList 基于动态数组,内存连续;LinkedList 基于双向链表,内存不连续。
- 随机访问:ArrayList 支持随机访问,通过索引直接定位元素,时间复杂度为 O(1);LinkedList 随机访问慢,需从头或尾遍历,时间复杂度为 O(n)。
- 增删操作:ArrayList 在中间位置增删元素,需移动大量元素,时间复杂度为 O(n);LinkedList 增删只需修改指针,时间复杂度为 O(1),但如果要定位到具体位置,仍需遍历,整体增删时间复杂度在 O(n) 左右。
- HashMap 在 JDK1.7 和 JDK1.8 的区别:
- 数据结构:JDK1.7 采用数组 + 链表;JDK1.8 采用数组 + 链表 + 红黑树。当链表长度大于 8 且数组长度大于 64 时,链表转化为红黑树,提高查找效率,因为红黑树的查找时间复杂度为 O(logn),链表为 O(n)。
- 插入方式:JDK1.7 采用头插法,多线程环境下可能形成环形链表;JDK1.8 采用尾插法,避免了环形链表问题。
- 扩容机制:JDK1.7 扩容时,需重新计算哈希值和索引位置,复制元素;JDK1.8 优化了扩容机制,部分元素在扩容时位置不变,减少了重新计算和复制的开销。
- Spring 框架中 IOC 和 AOP:
- IOC(控制反转):将对象的创建和管理从应用程序代码转移到 Spring 容器中。通过依赖注入(DI),容器将对象依赖的其他对象注入到目标对象中。例如,一个 Service 类依赖一个 Dao 类,在传统方式中,Service 类需自己创建 Dao 实例;在 Spring 中,由容器创建 Dao 实例并注入到 Service 中。这样降低了组件之间的耦合度,提高了代码的可维护性和可测试性。
- AOP(面向切面编程):将与业务逻辑无关的通用功能(如日志记录、事务管理、权限控制等)提取出来,形成一个个切面。这些切面在不修改原有业务代码的基础上,通过动态代理或字节码增强等技术,在目标方法执行前后或异常时等特定连接点织入通用功能。例如,在方法执行前记录日志,在方法执行后提交事务。
- 线程池的工作原理:
- 核心线程:线程池初始化时创建的一定数量的线程,这些线程会一直存活,除非设置了 allowCoreThreadTimeOut 为 true 且线程空闲时间超过了指定时间。
- 任务队列:当新任务提交时,如果核心线程都在忙碌,任务会被放入任务队列。任务队列有多种类型,如 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。
- 最大线程数:当任务队列已满,且核心线程都在忙碌时,线程池会创建新线程,直到达到最大线程数。
- 拒绝策略:当任务队列已满且线程数达到最大线程数时,新任务会根据拒绝策略处理。常见的拒绝策略有 AbortPolicy(抛出异常)、CallerRunsPolicy(由调用者线程处理任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,尝试提交新任务)。
- CountDownLatch:
- 它是 JUC 包下的一个同步工具类。允许一个或多个线程等待,直到其他线程完成一组操作。它通过一个计数器来实现,初始化时设置计数器的值,每完成一个操作,计数器减 1,当计数器减为 0 时,等待的线程被释放。例如,在一个多线程计算任务中,主线程需要等待所有子线程计算完成后再汇总结果,就可以使用 CountDownLatch。主线程调用 await() 方法等待,子线程完成任务后调用 countDown() 方法。
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,并使用 PreparedStatement 设置参数值,这样能有效防止 SQL 注入。例如,SQL 语句为 SELECT * FROM user WHERE username = #{username},实际执行时会变为 SELECT * FROM user WHERE username =?,然后通过 PreparedStatement.setString(1, "具体用户名") 设置参数。
- **{} 时,会直接将 {username}',如果 username 变量值为 'admin',实际执行的 SQL 就是 SELECT * FROM user WHERE username = 'admin'。由于是直接替换,所以容易导致 SQL 注入,如传入 'admin' OR '1' = '1',就会造成非法查询。
- Dubbo 的服务暴露和引用过程:
- 服务暴露:
- 服务提供者启动时,通过 Dubbo 配置文件或注解等方式定义要暴露的服务接口和实现类。
- Dubbo 框架根据配置信息,将服务接口和实现类封装成 Invoker 对象。
- 然后通过 ProxyFactory 生成服务的代理对象,这个代理对象负责将调用请求转换为远程调用。
- 接着,通过网络通信框架(如 Netty)将服务注册到注册中心(如 Zookeeper),注册中心记录服务提供者的地址、端口等信息。
- 服务引用:
- 服务消费者启动时,根据配置信息从注册中心订阅所需的服务。
- 注册中心将服务提供者的地址等信息返回给服务消费者。
- 服务消费者根据返回的信息,通过 ProxyFactory 生成服务的代理对象,这个代理对象负责将本地调用转换为远程调用。
- 当服务消费者调用代理对象的方法时,代理对象通过网络通信框架向服务提供者发起远程调用,并获取结果返回给服务消费者。
- 服务暴露:
- RabbitMQ 中的死信队列:
- 死信产生原因:
- 消息过期:设置了消息的过期时间(TTL),当消息在队列中存活时间超过 TTL 时,消息变为死信。
- 队列满:队列设置了最大长度,当队列满且有新消息进入时,最早的消息会变为死信。
- 消息被拒绝:消费者使用 basic.reject 或 basic.nack 方法拒绝消息,且设置 requeue 参数为 false 时,消息变为死信。
- 死信队列作用:可以将这些死信消息发送到另一个队列(死信队列)进行特殊处理,如记录日志、重新发送等,避免消息丢失,方便排查问题和进行后续处理。
- 死信产生原因:
- xxl - job 调度中心的核心原理:
- 调度注册:执行器(任务执行的机器)启动时,会向调度中心注册自己,包括执行器名称、地址等信息。调度中心维护一个执行器列表。
- 任务管理:在调度中心可以创建、修改、删除任务,设置任务的调度规则(如 cron 表达式)、执行器、任务参数等。
- 调度触发:调度中心根据任务的调度规则,定时触发任务。调度中心通过网络通信向对应的执行器发送任务执行请求。
- 任务执行:执行器接收到任务请求后,根据任务类型调用相应的任务执行逻辑,执行任务,并将执行结果返回给调度中心。
- 监控与报警:调度中心记录任务的执行日志、执行状态等信息,方便监控。同时,当任务执行失败等异常情况发生时,调度中心可以根据配置进行报警,通知相关人员。