第一轮面试 面试官:首先问几个基础问题。Java 中 ArrayList 和 LinkedList 的区别是什么? 王铁牛:ArrayList 是基于数组实现的,随机访问快,增删慢;LinkedList 基于链表实现,增删快,随机访问慢。 面试官:回答得不错。那 HashMap 在 JDK1.7 和 JDK1.8 中有什么主要区别? 王铁牛:1.8 之后引入了红黑树,当链表长度超过阈值会转成红黑树,提高查找效率,1.7 只有链表。 面试官:很好。Spring 框架中 IOC 和 AOP 分别是什么? 王铁牛:IOC 是控制反转,把对象创建和管理交给 Spring 容器;AOP 是面向切面编程,能在不修改代码情况下增加功能。 面试官:非常好,基础掌握得很扎实。
第二轮面试 面试官:接下来深入一些。多线程中线程池的核心参数有哪些,分别有什么作用? 王铁牛:嗯……有核心线程数,最大线程数,还有……还有队列容量吧,核心线程数就是一开始创建的线程数,最大线程数就是最多能创建的线程数。 面试官:那线程池的工作原理能详细说说吗? 王铁牛:就是有任务来了,先看核心线程有没有满,满了就放队列里,队列满了就创建新线程,到最大线程数就拒绝任务。 面试官:JVM 的内存模型了解吗,说说堆和栈的区别。 王铁牛:堆是存放对象实例的,栈是存放局部变量和方法调用的,堆是共享的,栈是线程私有的。
第三轮面试 面试官:最后问几个更难的。Dubbo 框架在分布式系统中有哪些应用场景,它的服务调用流程是怎样的? 王铁牛:嗯……就是在分布式里做服务治理,流程嘛,好像是消费者调用注册中心找服务,然后就调用提供者。 面试官:RabbitMQ 如何保证消息的可靠性,有哪些机制? 王铁牛:有确认机制,生产者发消息后能知道有没有发成功,还有持久化,消息存磁盘。 面试官:xxl - job 调度任务框架在实际项目中如何配置和使用,说说关键步骤。 王铁牛:嗯……要先引入依赖,然后配置一些参数,具体不太记得了。
面试官:好的,今天的面试就到这里。你整体基础知识掌握得还不错,但在一些深入的技术原理和实际应用场景方面,还需要再加强理解。回去等通知吧,我们会综合评估所有候选人后,再决定是否录用你。希望你后续能继续深入学习相关技术知识。
问题答案
- ArrayList 和 LinkedList 的区别:
- 数据结构:ArrayList 基于动态数组实现,内存连续;LinkedList 基于双向链表实现,节点分散存储。
- 随机访问:ArrayList 支持随机访问,通过索引直接定位元素,时间复杂度为 O(1);LinkedList 随机访问慢,需从头或尾遍历,时间复杂度为 O(n)。
- 增删操作:ArrayList 在中间或开头增删元素时,需移动大量元素,时间复杂度为 O(n);LinkedList 增删只需修改前后节点引用,时间复杂度为 O(1),但如果要先定位元素,整体时间复杂度也会受影响。
- 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 实例,使用 IOC 后,Spring 容器会创建 Dao 实例并注入到 Service 中,降低了代码耦合度。
- AOP(面向切面编程):将与业务逻辑无关但又贯穿多个业务模块的功能(如日志记录、事务管理、权限控制等)抽取出来形成切面。在不修改原有业务代码的情况下,通过动态代理或字节码增强等技术,将切面功能织入到目标方法执行的前后等位置。例如,在方法执行前记录日志,执行后提交事务等。
- 线程池的核心参数及工作原理:
- 核心参数:
- 核心线程数(corePoolSize):线程池在正常情况下保持的线程数,即使这些线程处于空闲状态也不会被销毁,除非设置了 allowCoreThreadTimeOut 为 true。
- 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列已满且核心线程都在忙碌时,线程池会创建新线程,直到达到最大线程数。
- 队列容量(workQueue):用于存放等待执行任务的队列。常见的队列类型有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)等。
- 线程存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。
- 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS 等。
- 拒绝策略(RejectedExecutionHandler):当任务队列已满且线程数达到最大线程数时,新任务的处理策略。常见策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(将任务交给调用者线程执行)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,尝试提交新任务)。
- 工作原理:当有新任务提交时,线程池首先检查核心线程是否都在执行任务。如果没有满,创建新的核心线程执行任务;如果核心线程已满,将任务放入队列。若队列已满,且线程数小于最大线程数,创建新的非核心线程执行任务。若线程数已达到最大线程数,根据拒绝策略处理新任务。
- 核心参数:
- JVM 的内存模型中堆和栈的区别:
- 存储内容:
- 堆:主要存放对象实例和数组,是 JVM 管理的最大一块内存区域,所有线程共享。对象的生命周期从创建到垃圾回收。
- 栈:每个线程都有自己的栈,用于存放局部变量表、操作数栈、动态链接、方法出口等信息。栈帧随着方法调用而创建,方法结束而销毁。局部变量包括基本数据类型和对象引用。
- 内存分配:
- 堆:堆内存分配是动态的,对象创建时在堆中分配内存,垃圾回收器负责回收不再使用的对象空间。
- 栈:栈内存分配是自动的,方法调用时为局部变量分配内存,方法结束时自动释放栈帧空间。
- 线程相关性:
- 堆:是共享的,多线程访问堆中的对象需要考虑线程安全问题。
- 栈:是线程私有的,每个线程的栈相互独立,不存在线程安全问题。
- 存储内容:
- Dubbo 框架在分布式系统中的应用场景及服务调用流程:
- 应用场景:
- 服务治理:在大型分布式系统中,服务数量众多,Dubbo 提供服务注册与发现功能,方便管理服务的上线、下线、动态扩容缩容等。
- 高性能 RPC 调用:适用于对性能要求较高的微服务间通信场景,通过高效的网络通信协议和序列化方式,实现快速的远程方法调用。
- 服务调用流程:
- 服务注册:服务提供者启动时,将自己提供的服务注册到注册中心(如 Zookeeper)。
- 服务订阅:服务消费者启动时,从注册中心订阅自己所需的服务。注册中心返回服务提供者的地址列表给消费者。
- 服务调用:消费者根据负载均衡算法从地址列表中选择一个服务提供者发起调用。调用过程中,Dubbo 会进行网络通信、序列化和反序列化等操作,将方法调用请求发送给提供者,提供者处理后返回结果。
- 应用场景:
- RabbitMQ 保证消息可靠性的机制:
- 消息确认机制(publisher confirm):生产者发送消息后,RabbitMQ 会给生产者发送确认消息,告知消息是否成功接收。生产者可以通过回调函数来处理确认结果,确保消息已成功发送到 RabbitMQ 服务器。
- 持久化:
- 队列持久化:通过将队列声明为持久化队列,RabbitMQ 重启后队列依然存在。
- 消息持久化:生产者将消息设置为持久化,RabbitMQ 会将消息写入磁盘,即使服务器重启,消息也不会丢失。但要注意,消息持久化并不能完全保证消息不丢失,如在消息刚写入内存还未刷盘时服务器宕机,消息仍可能丢失。
- 消费者确认机制(acknowledgment):消费者接收并处理完消息后,向 RabbitMQ 发送确认消息。RabbitMQ 只有收到确认消息后,才会将该消息从队列中删除。如果消费者未发送确认消息或连接断开,RabbitMQ 会认为消息未被成功处理,重新将消息分发给其他消费者或在一定时间后重新发送给该消费者。
- xxl - job 调度任务框架在实际项目中的配置和使用关键步骤:
- 引入依赖:在项目的 pom.xml 文件中添加 xxl - job 相关依赖,如 xxl - job - core 等。
- 配置调度中心:在 application.properties 或 application.yml 等配置文件中配置调度中心地址、访问令牌等信息。例如,配置调度中心的地址为:xxl.job.admin.addresses = http://127.0.0.1:8080/xxl - job - admin。
- 创建执行器:在项目中创建执行器,定义任务执行逻辑。可以通过继承 IJobHandler 类并重写 execute 方法来实现具体任务。例如:
@Component
@JobHandler(value = "demoJobHandler")
public class DemoJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
// 具体任务逻辑
System.out.println("执行任务:" + param);
return ReturnT.SUCCESS;
}
}
- 注册执行器:在调度中心中注册执行器,填写执行器名称、AppName、地址等信息。AppName 需与项目中配置的一致。
- 创建调度任务:在调度中心创建调度任务,选择对应的执行器、任务 handler、调度规则(如 cron 表达式定义执行时间)等,保存并启动任务,任务就会按照设定的规则执行。