第一轮面试 面试官:首先问你几个基础问题。Java 中 ArrayList 和 LinkedList 的区别是什么? 王铁牛:ArrayList 是基于数组实现的,随机访问快,增删慢;LinkedList 基于链表实现,增删快,随机访问慢。 面试官:回答得不错。那 HashMap 在 JDK1.7 和 JDK1.8 中有什么主要区别? 王铁牛:1.8 之后引入了红黑树,当链表长度达到 8 且数组长度达到 64 时,链表会转成红黑树,提高查找效率,1.7 没有。 面试官:很好。Spring 框架中 IOC 和 AOP 分别是什么? 王铁牛:IOC 是控制反转,把对象创建和管理的控制权交给 Spring 容器;AOP 是面向切面编程,在不修改原有代码的基础上,对业务进行增强。
第二轮面试 面试官:接下来深入一些。多线程中线程池的核心参数有哪些,分别有什么作用? 王铁牛:嗯……有核心线程数、最大线程数,还有……还有队列容量吧。核心线程数就是一直存活的线程数,最大线程数是能创建的最大线程数,队列容量就是存放任务的队列大小。 面试官:好。JVM 的垃圾回收机制了解吗,讲讲常用的垃圾回收算法。 王铁牛:嗯……有标记 - 清除算法,先标记可回收对象,然后清除。还有复制算法,把内存分成两块,将存活对象复制到另一块。还有……还有标记 - 整理算法,标记完后把存活对象整理到一起。 面试官:那 Spring Boot 自动配置原理是什么? 王铁牛:呃……就是 Spring Boot 能根据依赖自动配置一些 Bean 吧,具体不太清楚。
第三轮面试 面试官:最后一轮,难度再提升。Dubbo 的服务暴露和引用过程是怎样的? 王铁牛:嗯……好像是通过注册中心,服务提供者把服务暴露出去,服务消费者从注册中心获取服务,然后调用。具体细节不太记得了。 面试官:RabbitMQ 如何保证消息的可靠性? 王铁牛:可以开启确认机制,生产者发送消息后,RabbitMQ 会返回确认信息。还有……还有持久化,把队列和消息都持久化。 面试官:xxl - job 调度任务执行失败了,可能有哪些原因? 王铁牛:可能是网络问题,任务代码有异常,还有……还有调度配置有问题吧。
面试官:今天的面试就到这里,你回去等通知吧。我们会综合评估所有候选人,之后给你反馈。感谢你今天来参加面试。
问题答案:
- ArrayList 和 LinkedList 的区别:
- 数据结构:ArrayList 基于动态数组实现,它在内存中是连续存储的;LinkedList 基于双向链表实现,每个节点包含前驱和后继指针。
- 随机访问效率:ArrayList 支持随机访问,通过索引可以直接定位到元素,时间复杂度为 O(1);LinkedList 随机访问效率低,需要从头或尾开始遍历链表,时间复杂度为 O(n)。
- 增删效率:在 ArrayList 中间插入或删除元素时,需要移动大量元素,时间复杂度为 O(n);但在末尾插入或删除元素效率较高,时间复杂度为 O(1)。LinkedList 在任意位置增删元素,只需修改前后节点的指针,时间复杂度为 O(1)。
- 内存占用:ArrayList 由于是连续存储,内存利用率高,但如果数组扩容,会有一定的内存浪费;LinkedList 每个节点除了存储数据,还需要额外存储指针,内存占用相对较大。
- HashMap 在 JDK1.7 和 JDK1.8 中的区别:
- 数据结构:JDK1.7 中 HashMap 由数组 + 链表组成;JDK1.8 中当链表长度达到 8 且数组长度达到 64 时,链表会转成红黑树,即数据结构变为数组 + 链表 + 红黑树。这样在数据量较大且哈希冲突较多时,查找效率从 O(n)提升到 O(log n)。
- 哈希冲突解决方式:JDK1.7 采用头插法,在插入新元素时,新元素会插入到链表头部;JDK1.8 采用尾插法,新元素插入到链表尾部,避免了在多线程环境下头插法可能导致的环形链表问题。
- 扩容机制:JDK1.7 扩容时,需要重新计算每个元素的哈希值并重新插入到新的数组中;JDK1.8 优化了扩容机制,部分元素在扩容时不需要重新计算哈希值,直接根据原数组索引和新数组长度计算新的索引位置,提高了扩容效率。
- Spring 框架中 IOC 和 AOP:
- IOC(控制反转):传统编程中,对象的创建和管理由程序自身负责;而在 Spring 框架中,IOC 把对象的创建、初始化、销毁等控制权从应用程序转移到 Spring 容器,应用程序只需要使用对象即可。通过依赖注入(DI)的方式,Spring 容器将对象所依赖的其他对象注入到该对象中,实现解耦。例如,一个 Service 类依赖于一个 Dao 类,在 IOC 容器中,可以配置将 Dao 类的实例注入到 Service 类中,Service 类无需自己创建 Dao 实例。
- AOP(面向切面编程):AOP 是一种编程范式,它将横切关注点(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,形成独立的切面。在 Spring 中,通过代理模式实现 AOP。例如,对于多个业务方法都需要进行日志记录的场景,可以创建一个日志切面,在方法执行前、执行后或出现异常时记录日志,而不需要在每个业务方法中重复编写日志记录代码,从而提高代码的可维护性和复用性。
- 线程池的核心参数:
- 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁。当有新任务提交时,如果线程池中的线程数小于 corePoolSize,会创建新的线程来处理任务。
- 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列已满,且线程池中的线程数小于 maximumPoolSize 时,会继续创建新线程来处理任务。
- 队列容量(workQueue):用于存放等待执行的任务的队列。当线程池中的线程数达到 corePoolSize 后,新提交的任务会被放入队列中等待执行。常见的队列类型有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)、SynchronousQueue(同步队列,不存储任务,直接交给线程处理)等。
- 线程存活时间(keepAliveTime):当线程池中的线程数大于 corePoolSize 时,多余的空闲线程在存活时间内没有接到新任务,会被销毁。
- 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
- JVM 的垃圾回收算法:
- 标记 - 清除算法:分为标记和清除两个阶段。首先标记出所有需要回收的对象,然后在标记完成后统一回收所有被标记的对象。这种算法的缺点是会产生大量不连续的内存碎片,导致后续大对象无法分配足够的连续内存。
- 复制算法:将内存分为大小相等的两块,每次只使用其中一块。当这块内存用完时,将存活对象复制到另一块内存上,然后清除使用过的那一块内存。这种算法适用于对象存活率较低的场景,如新生代。但它的缺点是内存利用率低,因为总有一半内存处于闲置状态。
- 标记 - 整理算法:先标记出所有需要回收的对象,然后将存活对象向一端移动,最后清除边界以外的内存。这种算法解决了标记 - 清除算法产生内存碎片的问题,适用于对象存活率较高的场景,如老年代。
- Spring Boot 自动配置原理:
- Spring Boot 依赖于 Spring 框架,它通过大量的自动配置类(以 @Configuration 注解标注)来实现自动配置。这些自动配置类会根据项目中引入的依赖和配置属性来决定是否生效。例如,当项目中引入了 spring - boot - starter - jdbc 依赖时,Spring Boot 会自动配置数据源、JdbcTemplate 等相关的 Bean。
- Spring Boot 使用 @Conditional 注解及其一系列衍生注解(如 @ConditionalOnClass、@ConditionalOnProperty 等)来实现条件化配置。@ConditionalOnClass 表示当类路径下存在某个类时,该自动配置类才生效;@ConditionalOnProperty 表示当配置文件中存在某个属性时,该自动配置类才生效。通过这些条件判断,Spring Boot 可以根据项目的实际情况自动配置合适的 Bean,大大简化了 Spring 应用的配置过程。
- Dubbo 的服务暴露和引用过程:
- 服务暴露:
- 服务提供者启动时,将服务接口和实现类通过 Dubbo 配置文件或注解的方式进行配置。
- Dubbo 框架根据配置信息,使用 ProxyFactory 生成服务的代理对象。
- 服务提供者通过网络通信框架(如 Netty)将服务注册到注册中心(如 Zookeeper),注册信息包括服务接口、服务地址等。
- 服务引用:
- 服务消费者启动时,根据配置从注册中心订阅所需的服务。
- 注册中心将服务提供者的地址列表返回给服务消费者。
- 服务消费者使用 ProxyFactory 生成服务的代理对象,通过代理对象调用服务提供者的方法。在调用过程中,Dubbo 会根据负载均衡策略(如随机、轮询等)从服务提供者地址列表中选择一个地址进行调用。
- 服务暴露:
- RabbitMQ 保证消息可靠性:
- 消息持久化:
- 队列持久化:通过设置队列的 durable 属性为 true,将队列持久化到磁盘。这样即使 RabbitMQ 服务器重启,队列依然存在。
- 消息持久化:通过设置消息的 deliveryMode 属性为 2,将消息标记为持久化。这样消息会在被投递到队列之前先写入磁盘,保证消息不会因服务器重启而丢失。
- 确认机制:
- 生产者确认(publisher confirm):生产者发送消息后,RabbitMQ 会返回一个确认信息给生产者,告知消息是否成功到达服务器。生产者可以通过实现 ConfirmCallback 接口来处理确认结果。
- 消费者确认(consumer ack):消费者接收并处理完消息后,需要向 RabbitMQ 发送一个确认消息(ACK),告知 RabbitMQ 可以安全地删除该消息。如果 RabbitMQ 没有收到 ACK,会认为消息处理失败,可能会重新投递该消息。
- 消息持久化:
- xxl - job 调度任务执行失败可能原因:
- 网络问题:调度中心与执行器之间的网络不稳定或中断,导致调度中心无法将任务发送给执行器,或者执行器执行结果无法返回给调度中心。
- 任务代码异常:任务代码中存在逻辑错误、空指针异常、数据库连接异常等,导致任务执行失败。例如,任务需要查询数据库,但数据库连接配置错误,无法获取数据,从而使任务执行失败。
- 调度配置问题:调度时间设置不合理,如设置的调度时间与任务实际执行时间冲突;或者执行器配置错误,如执行器注册信息不正确,调度中心无法找到对应的执行器来执行任务。
- 资源不足:执行器所在服务器的 CPU、内存、磁盘等资源不足,导致任务无法正常执行。例如,任务需要处理大量数据,但服务器内存不足,无法加载数据,从而使任务执行失败。