第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时是怎样的机制? 王铁牛:当 ArrayList 元素个数达到容量阈值时,会进行扩容,新容量是原容量的 1.5 倍,然后把原数组内容复制到新数组。 面试官:很好。HashMap 在 JDK1.8 中,链表转红黑树的条件是什么? 王铁牛:当链表长度大于 8 且数组容量大于等于 64 时,链表会转成红黑树。
第二轮面试 面试官:接下来聊聊多线程和线程池。创建线程有几种方式? 王铁牛:有三种,继承 Thread 类、实现 Runnable 接口、实现 Callable 接口并配合 FutureTask 使用。 面试官:那线程池的核心参数有哪些,分别代表什么意思? 王铁牛:嗯……有核心线程数、最大线程数,还有……还有队列容量吧,核心线程数就是一直存活的线程数,最大线程数是能创建的最大线程数,队列容量就是存放任务的队列大小。 面试官:那线程池在什么情况下会拒绝任务? 王铁牛:呃……当队列满了,而且线程数达到最大线程数的时候吧。
第三轮面试 面试官:再谈谈框架相关的,Spring 中 Bean 的作用域有哪些? 王铁牛:有 singleton、prototype、request、session 这些,singleton 是单例,prototype 是多例。 面试官:Spring Boot 自动配置的原理是什么? 王铁牛:就是……它会根据类路径下的依赖,自动配置一些 Bean 吧,具体怎么实现的我不太清楚。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 是直接拼接 SQL,可能有 SQL 注入风险。 面试官:Dubbo 服务调用的流程是怎样的? 王铁牛:嗯……好像是服务提供者注册服务,消费者从注册中心获取服务,然后进行调用,具体细节我不太记得了。 面试官:RabbitMQ 有哪些常见的交换机类型? 王铁牛:有 direct、topic、fanout,direct 是根据路由键精准匹配,topic 是根据通配符匹配,fanout 是广播给所有队列。 面试官:xxl - job 任务调度的核心原理是什么? 王铁牛:这个……我只知道它能实现分布式任务调度,具体原理不太了解。 面试官:Redis 有哪些数据类型,分别适用于什么场景? 王铁牛:有 string、hash、list、set、zset,string 存简单数据,hash 存对象,list 存列表数据,set 存不重复数据,zset 存有序不重复数据。
面试总结:从这轮面试来看,你对于一些基础的知识点掌握得还不错,像 ArrayList、HashMap 的底层结构,创建线程的方式等回答得都比较准确。但是在一些进阶和框架原理方面,比如 Spring Boot 自动配置原理、Dubbo 服务调用流程、xxl - job 任务调度核心原理等,回答得不是很清晰,还有提升的空间。我们后续会综合评估所有面试者的情况,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。
问题答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构,它允许以数组下标的方式快速访问元素。这种结构适合频繁的随机访问操作。例如,在需要快速定位某个元素位置的场景,如学生成绩按顺序记录,通过下标快速获取某个学生成绩时,ArrayList 就很适用。
- HashMap:JDK1.8 之前,底层是数组加链表结构。数组的每个位置是一个链表的头节点,通过哈希算法计算 key 的哈希值,再对数组长度取模得到数组下标,将键值对存放在对应下标的链表中。JDK1.8 后,当链表长度大于 8 且数组容量大于等于 64 时,链表会转化为红黑树,以提高查找效率。在需要快速根据 key 查找 value 的场景,如用户信息通过用户 ID 存储和查找,HashMap 能提供高效的操作。
- ArrayList 扩容机制:
- ArrayList 有一个容量的概念,当向 ArrayList 中添加元素,元素个数达到容量阈值(一般是当前容量)时,会触发扩容。新容量是原容量的 1.5 倍(原容量右移一位再加原容量)。然后会创建一个新的数组,将原数组中的元素复制到新数组中。这是为了在保证 ArrayList 能动态扩展存储元素的同时,尽量减少频繁扩容带来的性能开销。例如,在不断添加用户信息到 ArrayList 时,当达到容量限制就会按此机制扩容。
- HashMap 链表转红黑树条件:
- 当链表长度大于 8 且数组容量大于等于 64 时,链表会转成红黑树。这是因为链表在长度较短时,查找效率虽然是 O(n),但由于数据量小,性能影响不大。而当链表过长,查找效率会显著下降,转成红黑树后,查找效率能提升到 O(logn)。例如,在大量数据按哈希分布到某个桶中形成长链表时,转成红黑树能优化查找性能。
- 创建线程的方式:
- 继承 Thread 类:通过继承 Thread 类,重写 run 方法,在 run 方法中编写线程执行的逻辑。例如:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行逻辑");
}
}
MyThread thread = new MyThread();
thread.start();
- 实现 Runnable 接口:实现 Runnable 接口的 run 方法,将实现类的实例作为参数传递给 Thread 类的构造函数。例如:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行逻辑");
}
}
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
- 实现 Callable 接口并配合 FutureTask 使用:实现 Callable 接口的 call 方法,该方法有返回值。通过 FutureTask 包装 Callable 实现类的实例,再将 FutureTask 作为参数传递给 Thread 类的构造函数。例如:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "线程执行结果";
}
}
MyCallable callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁。例如,在一个 Web 服务器中,核心线程数可以设置为能处理常见请求量的线程数量,保证这些线程随时可用,处理新的请求。
- 最大线程数(maximumPoolSize):线程池能创建的最大线程数。当任务队列已满,且核心线程都在忙碌时,线程池会创建新的线程,直到达到最大线程数。例如,在高并发的电商抢购场景,可能会临时创建更多线程来处理大量请求,但不能超过最大线程数。
- 队列容量:用于存放等待执行任务的队列大小。当核心线程都在忙碌时,新的任务会被放入队列中等待。例如,使用 ArrayBlockingQueue 可以设置固定大小的队列,当队列满了,再提交任务就可能触发线程池的拒绝策略。
- 线程池拒绝任务的情况:
- 当队列已满,且线程数达到最大线程数时,线程池会拒绝新提交的任务。线程池提供了几种拒绝策略,如 AbortPolicy(默认策略,直接抛出 RejectedExecutionException 异常)、CallerRunsPolicy(将任务交给调用者线程执行)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交新任务)。例如,在一个订单处理系统中,当高并发时,任务队列满且线程数达到最大,若采用 AbortPolicy 策略,新的订单处理任务提交时就会抛出异常。
- Spring 中 Bean 的作用域:
- singleton:单例模式,在整个 Spring 容器中,只会创建一个该 Bean 的实例。例如,数据库连接池的配置 Bean,通常设置为 singleton,保证整个应用使用同一个连接池实例,避免资源浪费。
- prototype:多例模式,每次从容器中获取该 Bean 时,都会创建一个新的实例。例如,处理用户请求的一些业务逻辑 Bean,如果每个请求都需要独立的实例来处理,避免相互干扰,就可以设置为 prototype。
- request:在一次 HTTP 请求中,只会创建一个该 Bean 的实例。适用于 Web 应用中,与一次请求相关的 Bean,如记录本次请求相关信息的 Bean。
- session:在一个 HTTP Session 中,只会创建一个该 Bean 的实例。用于与用户会话相关的 Bean,如记录用户登录状态等信息的 Bean。
- Spring Boot 自动配置原理:
- Spring Boot 利用了 Spring 框架的条件化配置(@Conditional)机制。在启动过程中,Spring Boot 会扫描 classpath 下的依赖包,根据这些依赖包的存在与否以及一些配置属性,来决定是否自动配置某个 Bean。例如,当 classpath 下存在 spring - jdbc 依赖时,Spring Boot 会自动配置数据源、JdbcTemplate 等相关的 Bean。它通过一系列的自动配置类(以 AutoConfiguration 结尾),这些类上使用了 @Conditional 注解及其相关的派生注解,如 @ConditionalOnClass(当某个类存在时才生效)、@ConditionalOnProperty(当某个配置属性满足条件时才生效)等。同时,Spring Boot 还使用了 Spring Factories 机制,在 META - INF/spring.factories 文件中定义了自动配置类,Spring Boot 启动时会加载这些自动配置类进行自动配置。
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行设置参数值。这种方式能有效防止 SQL 注入,因为参数值是作为字符串传入,不会被解析为 SQL 语句的一部分。例如,SQL 语句为
SELECT * FROM user WHERE username = #{username},实际执行时会变为SELECT * FROM user WHERE username =?,然后通过 PreparedStatement 设置参数值。 - **{} 时,会直接将 {username}'
,如果 username 变量被恶意赋值为' OR '1' = '1',就会导致 SQL 注入,实际执行的 SQL 变为SELECT * FROM user WHERE username = '' OR '1' = '1'`,会返回所有用户数据。
- #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行设置参数值。这种方式能有效防止 SQL 注入,因为参数值是作为字符串传入,不会被解析为 SQL 语句的一部分。例如,SQL 语句为
- Dubbo 服务调用流程:
- 服务注册:服务提供者启动时,将自己提供的服务注册到注册中心(如 Zookeeper)。它会将服务接口、服务实现类、服务版本等信息注册到注册中心的指定节点。
- 服务订阅:服务消费者启动时,从注册中心订阅自己需要的服务。注册中心会将服务提供者的地址列表返回给消费者。
- 服务调用:消费者根据从注册中心获取的服务提供者地址列表,选择一个地址(可以通过负载均衡算法,如随机、轮询等),然后通过网络通信(如 Netty)与服务提供者建立连接,发起远程方法调用。服务提供者接收到调用请求后,执行相应的服务方法,并将结果返回给消费者。例如,在一个电商微服务架构中,商品服务作为服务提供者,订单服务作为消费者,订单服务需要调用商品服务获取商品信息,就会按照这个流程进行服务调用。
- RabbitMQ 常见交换机类型:
- direct:直连交换机,根据路由键(routing key)精准匹配。消息发送时指定的路由键与队列绑定的路由键完全匹配时,消息会被发送到该队列。例如,在一个订单系统中,订单创建消息可以根据订单类型的路由键,发送到专门处理该类型订单的队列。
- topic:主题交换机,根据通配符匹配。路由键支持两种通配符,# 表示匹配一个或多个单词,* 表示匹配一个单词。例如,路由键为
order.#可以匹配order.create、order.pay等,order.*只能匹配order.create这样一个单词的路由键。适用于需要根据一定规则灵活分发消息的场景。 - fanout:扇形交换机,不处理路由键,直接将消息广播给所有绑定的队列。例如,在一个系统监控场景中,监控消息可能需要同时发送到多个不同用途的队列,如日志记录队列、报警队列等,就可以使用 fanout 交换机。
- xxl - job 任务调度核心原理:
- 调度中心:是 xxl - job 的核心组件,负责任务的管理、调度触发等。它维护了任务的基本信息,如任务 ID、任务执行器地址、任务执行周期等。调度中心基于 Quartz 框架实现定时任务调度,通过数据库存储任务信息和调度日志。
- 执行器:部署在各个业务系统中,负责接收调度中心发送的任务并执行。执行器启动时会向调度中心注册自己,包括执行器名称、地址等信息。调度中心根据任务配置,定时触发任务,将任务发送给对应的执行器。执行器接收到任务后,通过反射调用具体的任务执行方法,并将执行结果返回给调度中心。例如,在一个电商系统中,执行器可以部署在订单模块,负责执行订单超时取消等定时任务。
- Redis 数据类型及适用场景:
- string:最基本的数据类型,能存储字符串、数字等简单数据。适用于缓存用户信息、配置参数等场景。例如,缓存用户的登录状态,将用户 ID 作为 key,登录状态(如已登录、未登录)作为 value 存储。
- hash:适合存储对象,以字段 - 值的形式存储。例如,存储用户详细信息,将用户 ID 作为 key,用户的各个属性(如姓名、年龄、地址)作为字段,对应的值作为 value 存储。
- list:是一个链表结构,适合存储列表数据,支持在列表两端进行插入和删除操作。例如,实现简单的消息队列,将消息依次插入到 list 的一端,从另一端取出消息进行处理。
- set:无序不重复的集合,适合存储不重复的数据,如统计网站的独立访客,将访客 ID 存储在 set 中,利用其不重复特性统计数量。
- zset:有序不重复的集合,每个元素都关联一个分数(score),根据分数进行排序。适用于排行榜场景,如游戏玩家的积分排行榜,将玩家 ID 作为元素,积分作为分数存储在 zset 中,通过分数排序获取排行榜信息。