第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 之后还有红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时是怎么操作的? 王铁牛:当元素个数达到容量阈值时,会创建一个新的更大的数组,然后把旧数组的元素复制到新数组。 面试官:很好。HashMap 在 JDK1.8 中,链表转红黑树的条件是什么? 王铁牛:当链表长度达到 8 且数组容量大于等于 64 时,链表会转成红黑树。
第二轮面试 面试官:接下来聊聊多线程和线程池。创建线程有几种方式? 王铁牛:有三种,继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。 面试官:那线程池的核心参数有哪些,分别有什么作用? 王铁牛:有核心线程数、最大线程数、存活时间、阻塞队列。核心线程数是线程池初始化的线程数,最大线程数是线程池能容纳的最大线程数,存活时间是线程空闲多久会被销毁,阻塞队列用来存放任务。 面试官:那线程池的拒绝策略有哪些? 王铁牛:呃……有 AbortPolicy,直接抛出异常;还有 DiscardPolicy,丢弃任务不抛异常,其他的……我不太记得了。
第三轮面试 面试官:再谈谈框架相关,Spring 中 Bean 的作用域有哪些? 王铁牛:有 singleton 单例、prototype 原型、request、session 这些。 面试官:Spring Boot 自动配置的原理是什么? 王铁牛:嗯……好像是通过一些注解和配置类,自动帮我们配置一些东西,具体不太清楚。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 能防止 SQL 注入。 面试官:Dubbo 服务调用的流程是怎样的? 王铁牛:呃,就是服务提供者注册服务,消费者从注册中心获取服务,然后调用,具体细节不太熟。 面试官:RabbitMQ 有哪些常见的交换机类型? 王铁牛:有 direct、topic、fanout,好像还有个 headers 。 面试官:xxl - job 任务调度的核心原理是什么? 王铁牛:这个……不太了解。 面试官:Redis 有哪些数据类型,分别适用于什么场景? 王铁牛:有 string、hash、list、set、zset 。string 存简单数据,hash 存对象,list 存有序列表,set 存无序不重复数据,zset 存有序且带分数的数据。
面试总结:从这轮面试来看,你在一些基础知识方面掌握得还不错,像 ArrayList、HashMap 的底层结构,创建线程的方式等回答得比较准确。但在一些进阶和框架相关知识上,比如 Spring Boot 自动配置原理、Dubbo 服务调用流程、xxl - job 任务调度原理等,还存在不足。回去之后可以针对这些薄弱点再深入学习。今天的面试就到这里,你回家等通知吧。
答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构,它允许我们以数组的方式顺序存储元素,方便随机访问。例如在需要频繁根据索引获取元素的场景下很适用,像学生成绩列表,通过索引获取某个学生成绩。
- HashMap:JDK1.8 之前底层是数组加链表,JDK1.8 之后是数组加链表加红黑树。数组的每个位置是一个桶(bucket),当发生哈希冲突时,会在桶中以链表形式存储元素。当链表长度达到 8 且数组容量大于等于 64 时,链表会转成红黑树,以提高查找效率。适用于需要快速根据键获取值的场景,比如用户信息存储,通过用户 ID 快速获取用户信息。
- ArrayList 扩容操作:当 ArrayList 中元素个数达到容量阈值(默认为数组长度的 0.75 倍)时,会创建一个新的数组,新数组的容量是原数组容量的 1.5 倍(原容量右移一位再加原容量)。然后通过 System.arraycopy 方法将旧数组中的元素复制到新数组中。这是为了在保证 ArrayList 能动态扩展容量的同时,尽量减少内存分配和数据复制的开销。
- HashMap 链表转红黑树条件:当链表长度达到 8 且数组容量大于等于 64 时,链表会转成红黑树。这是因为链表在长度较短时,查找效率虽然是 O(n),但由于数据量小,性能影响不大。而当链表过长时,查找效率会显著下降,转成红黑树后,查找效率可以提高到 O(logn),从而提升整体性能。
- 创建线程的方式:
- 继承 Thread 类:通过继承 Thread 类,重写 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 接口:实现 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<Integer> {
@Override
public Integer call() throws Exception {
return 10;
}
}
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
Integer result = futureTask.get();
System.out.println("线程返回结果:" + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池初始化时创建的线程数,这些线程会一直存活,即使处于空闲状态,除非设置了 allowCoreThreadTimeOut 为 true。例如在一个 Web 服务器中,核心线程数可以设置为能处理日常平均请求量的线程数。
- 最大线程数(maximumPoolSize):线程池能容纳的最大线程数。当任务队列已满且核心线程都在忙碌时,线程池会创建新的线程,直到达到最大线程数。比如在电商大促期间,可能需要临时增加线程数来处理大量的订单请求,最大线程数就决定了能处理的极限。
- 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程在多长时间后会被销毁。例如在一个任务量有波动的系统中,当任务量减少时,多余的线程在存活时间过后会被销毁,以节省资源。
- 阻塞队列(workQueue):用于存放等待执行的任务。常见的阻塞队列有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)、SynchronousQueue(同步队列)等。不同的阻塞队列适用于不同的场景,比如 ArrayBlockingQueue 适用于需要限制任务数量的场景,LinkedBlockingQueue 适用于任务量较大且希望能缓存较多任务的场景。
- 线程池拒绝策略:
- AbortPolicy:直接抛出 RejectedExecutionException 异常,阻止系统正常运行。适用于需要严格控制任务执行数量,不允许丢弃任务的场景。
- DiscardPolicy:丢弃任务,不抛出异常。适用于对任务执行结果不敏感,允许丢弃任务的场景,比如一些日志记录任务。
- DiscardOldestPolicy:丢弃队列中最老的任务(即将被执行的任务),然后尝试提交新任务。适用于希望优先处理新任务的场景。
- CallerRunsPolicy:由调用线程(提交任务的线程)来执行任务。适用于希望降低新任务提交速度,减轻线程池压力的场景。
- Spring 中 Bean 的作用域:
- singleton:单例模式,在整个 Spring 容器中,只会创建一个实例。例如配置数据库连接池的 Bean,使用单例模式可以保证整个应用中只有一个连接池实例,避免资源浪费。
- prototype:原型模式,每次请求获取 Bean 时,都会创建一个新的实例。比如创建一个处理用户请求的 Bean,每个用户请求都创建一个新的实例,避免不同请求之间的状态干扰。
- request:在一次 HTTP 请求中,只会创建一个实例。适用于 Web 应用中,每个请求需要一个独立的 Bean 实例来处理请求相关业务。
- session:在一个 HTTP Session 中,只会创建一个实例。适用于需要在用户会话期间共享数据的场景,比如记录用户登录状态的 Bean。
- Spring Boot 自动配置原理:Spring Boot 基于条件配置(@Conditional 注解)和 Spring Factories 机制实现自动配置。在 Spring Boot 的启动过程中,会扫描 META - INF/spring.factories 文件,该文件中定义了各种自动配置类。这些自动配置类会根据当前项目的依赖和配置条件,决定是否生效。例如,如果项目中引入了 spring - data - jpa 依赖,那么相关的 JPA 自动配置类就会生效,自动配置数据源、EntityManagerFactory 等。
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行参数设置,这样可以有效防止 SQL 注入。例如:
SELECT * FROM user WHERE username = #{username},在执行时会将 #{username} 替换为?,然后通过 PreparedStatement 的 setString 方法设置参数值。 - **{} 时,会直接将 {username}'`,如果 username 变量值为 “admin' OR '1'='1”,就会导致 SQL 注入。所以 ${} 一般用于传入数据库对象,如表名、列名等,但使用时要特别小心。
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行参数设置,这样可以有效防止 SQL 注入。例如:
- Dubbo 服务调用流程:
- 服务注册:服务提供者启动时,将自己提供的服务注册到注册中心(如 Zookeeper),注册中心记录服务提供者的地址和服务接口等信息。
- 服务订阅:服务消费者启动时,从注册中心订阅自己需要的服务,注册中心返回服务提供者的地址列表。
- 服务调用:服务消费者根据负载均衡算法从地址列表中选择一个服务提供者地址,然后通过网络通信(如 Netty)调用服务提供者的接口方法。在调用过程中,可能还会涉及到序列化、反序列化等操作。
- RabbitMQ 常见交换机类型:
- direct:直连交换机,根据路由键(routing key)将消息发送到对应的队列。如果路由键匹配,消息就会被发送到该队列。例如,订单系统中,根据订单类型的路由键,将订单消息发送到不同的订单处理队列。
- topic:主题交换机,支持通配符匹配路由键。例如,路由键为 “order.#” 可以匹配 “order.create”、“order.update” 等以 “order.” 开头的所有路由键,消息会被发送到匹配的队列。适用于需要根据一定规则灵活分发消息的场景。
- fanout:扇出交换机,不处理路由键,将接收到的消息广播到所有绑定的队列。比如在一个系统中,需要将系统通知消息发送到多个不同用途的队列,就可以使用扇出交换机。
- headers:headers 交换机根据消息的 headers 属性来匹配队列,不常用。
- xxl - job 任务调度核心原理:xxl - job 由调度中心、执行器和任务组成。调度中心负责任务的管理、调度触发。它通过数据库存储任务信息、调度日志等。执行器是任务的执行载体,负责接收调度中心的调度请求并执行任务。调度中心通过定时任务(如 Quartz)触发任务调度,根据任务配置的调度策略(如 cron 表达式)计算下次调度时间。当到达调度时间时,调度中心向执行器发送调度请求,执行器接收到请求后,调用具体的任务逻辑进行执行,并将执行结果返回给调度中心。
- Redis 数据类型及适用场景:
- string:最基本的数据类型,可存储字符串、数字、二进制数据等。适用于缓存简单数据,如用户登录信息、商品基本信息等。例如缓存用户的昵称,通过用户 ID 作为键,昵称作为值存储。
- hash:用于存储对象,以字段 - 值(field - value)的形式存储。适用于存储一个对象的多个属性,比如存储用户的详细信息,每个属性作为一个字段。
- list:有序的字符串列表,支持从两端插入和删除元素。适用于实现消息队列、最新消息展示等场景。比如实现一个简单的消息队列,生产者向 list 一端插入消息,消费者从另一端取出消息。
- set:无序且不重复的字符串集合。适用于需要去重的场景,如统计网站的独立访客,将访客 ID 存储在 set 中,天然去重。
- zset:有序集合,每个元素都关联一个分数(score),根据分数排序。适用于排行榜场景,比如游戏玩家的积分排行榜,通过分数来排序展示玩家名次。