第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:当 ArrayList 元素个数达到容量阈值时,会创建一个新的数组,新数组容量是原数组的 1.5 倍,然后把原数组的元素复制到新数组。 面试官:很好。HashMap 在 JDK1.8 中,链表转红黑树的条件是什么? 王铁牛:当链表长度大于等于 8 且数组容量大于等于 64 时,链表会转成红黑树。
第二轮面试 面试官:接下来聊聊多线程和线程池。创建线程有几种方式? 王铁牛:有三种,继承 Thread 类、实现 Runnable 接口、实现 Callable 接口并搭配 FutureTask 使用。 面试官:那线程池的核心参数有哪些,分别代表什么含义? 王铁牛:嗯……有核心线程数、最大线程数,还有……还有队列容量吧。核心线程数就是线程池一开始创建的线程数量,最大线程数是能创建的最大线程数,队列容量就是存放任务的队列大小。 面试官:那线程池的拒绝策略有哪些? 王铁牛:呃……好像有 AbortPolicy,直接抛出异常,还有 DiscardPolicy,丢弃任务不抛出异常,其他的我不太确定了。
第三轮面试 面试官:再谈谈框架相关的,Spring 中 Bean 的作用域有哪些? 王铁牛:有 singleton 单例、prototype 原型、request、session 这些。 面试官:Spring Boot 自动配置的原理是什么? 王铁牛:就是……它会根据类路径下的依赖和配置文件,自动配置一些 Bean 吧,具体原理我不太能说清楚。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 能防止 SQL 注入,${} 不能。 面试官:Dubbo 服务暴露的过程是怎样的? 王铁牛:嗯……好像是通过注册中心,然后……然后进行一些配置啥的,具体细节我不太记得了。 面试官:RabbitMQ 的工作模式有哪些? 王铁牛:有简单模式、工作队列模式、发布订阅模式,还有……还有其他几种,我一下子想不起来了。 面试官:xxl - job 任务调度的核心原理是什么? 王铁牛:就是……它有个调度中心,然后去调度任务,具体怎么调度的我不太清楚。 面试官:Redis 有哪些数据类型,分别适用于什么场景? 王铁牛:有 String、Hash、List、Set、ZSet。String 可以存简单数据,Hash 适合存对象,List 可以做队列,Set 可以去重,ZSet 可以排序,具体场景我不太会详细说。
面试总结:从这三轮面试来看,你在一些基础知识上掌握得还不错,像 ArrayList、HashMap 的底层结构,创建线程的方式等回答得比较准确。但在一些进阶和框架原理方面,比如 Spring Boot 自动配置原理、Dubbo 服务暴露过程、xxl - job 任务调度核心原理等,回答得不是很清晰,还有提升的空间。我们后续会综合评估所有面试者的情况,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。
问题答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构,它允许我们以数组下标的方式快速访问元素。例如,
ArrayList<Integer> list = new ArrayList<>(); list.add(10); int num = list.get(0);这里通过get(0)能快速获取到添加的第一个元素。数组结构的优点是查询效率高,时间复杂度为 O(1),但在插入和删除元素时,如果不是在末尾操作,需要移动大量元素,时间复杂度为 O(n)。 - HashMap:JDK1.8 之前底层是数组加链表结构,JDK1.8 后引入了红黑树。数组的每个位置称为桶(bucket),当发生哈希冲突时,会将冲突的元素以链表的形式存储在桶中。当链表长度大于等于 8 且数组容量大于等于 64 时,链表会转成红黑树,以提高查找效率。HashMap 通过 key 的哈希值来确定元素在数组中的位置,其平均查找时间复杂度为 O(1),但在哈希冲突严重时,链表过长,查找时间复杂度会退化为 O(n),红黑树的引入优化了这种极端情况下的查找效率。
- ArrayList:底层是数组结构,它允许我们以数组下标的方式快速访问元素。例如,
- ArrayList 扩容操作:
- ArrayList 有一个容量(capacity)和实际元素个数(size)。当
size达到capacity时,就会触发扩容。扩容时,会创建一个新的数组,新数组的容量是原数组容量的 1.5 倍(newCapacity = oldCapacity + (oldCapacity >> 1))。然后通过System.arraycopy()方法将原数组的元素复制到新数组中。例如:
这样就完成了数组的扩容和元素复制。int[] oldArray = {1, 2, 3}; int[] newArray = new int[oldArray.length * 1.5]; System.arraycopy(oldArray, 0, newArray, 0, oldArray.length); - ArrayList 有一个容量(capacity)和实际元素个数(size)。当
- HashMap 链表转红黑树条件:
- 当链表长度大于等于 8 且数组容量大于等于 64 时,链表会转成红黑树。这是因为链表在长度较短时,查询效率虽然会因为哈希冲突而有所下降,但整体性能还可以接受。而当链表过长时,查询时间复杂度退化为 O(n),效率大幅降低。红黑树是一种自平衡的二叉查找树,其查找、插入、删除的时间复杂度均为 O(log n),在链表过长时转换为红黑树能显著提高查找效率。
- 创建线程的方式:
- 继承 Thread 类:通过继承
Thread类,重写run()方法来定义线程执行的逻辑。例如:
class MyThread extends Thread { @Override public void run() { System.out.println("Thread is running"); } } MyThread thread = new MyThread(); thread.start();- 实现 Runnable 接口:实现
Runnable接口的run()方法,然后将实现类的实例作为参数传递给Thread类的构造函数。例如:
class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable is running"); } } MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start();- 实现 Callable 接口并搭配 FutureTask 使用:
Callable接口的call()方法可以有返回值,并且可以抛出异常。FutureTask类实现了RunnableFuture接口,既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。例如:
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 100; } } 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: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } - 继承 Thread 类:通过继承
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池在初始化后,会创建
corePoolSize数量的线程来处理任务。这些线程即使在空闲状态下也不会被销毁(除非设置了allowCoreThreadTimeOut为true)。例如,一个线程池设置corePoolSize为 5,那么初始化后就会有 5 个线程随时准备处理任务。 - 最大线程数(maximumPoolSize):线程池能创建的最大线程数量。当任务队列已满,且当前线程数小于
maximumPoolSize时,线程池会创建新的线程来处理任务。但线程数不会超过maximumPoolSize。比如,maximumPoolSize设置为 10,当任务过多,核心线程数 5 个处理不过来,且队列已满时,最多还能再创建 5 个线程,总共 10 个线程处理任务。 - 队列容量(workQueue):用于存放等待处理任务的队列。常用的队列类型有
ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列,默认容量为Integer.MAX_VALUE)等。当核心线程都在忙碌时,新的任务会被放入队列中等待处理。例如,使用ArrayBlockingQueue作为任务队列,设置容量为 100,那么最多可以存放 100 个任务等待核心线程处理。 - 线程存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程在等待新任务到来的时间超过
keepAliveTime后,会被销毁。例如,keepAliveTime设置为 60 秒,当有多余的线程空闲时间超过 60 秒,就会被销毁。 - 时间单位(unit):
keepAliveTime的时间单位,如TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)等。
- 核心线程数(corePoolSize):线程池在初始化后,会创建
- 线程池拒绝策略:
- AbortPolicy:这是默认的拒绝策略,当任务无法提交到线程池(队列已满且线程数达到最大线程数)时,直接抛出
RejectedExecutionException异常。例如:
运行上述代码,当任务数超过队列容量和最大线程数时,就会抛出异常。ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 4, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.AbortPolicy()); for (int i = 0; i < 8; i++) { executor.submit(() -> System.out.println(Thread.currentThread().getName() + " is running")); }- DiscardPolicy:丢弃任务,不抛出异常。当任务无法提交到线程池时,默默丢弃该任务,不会有任何提示。例如:
这里多余的任务会被丢弃,但程序不会报错。ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 4, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.DiscardPolicy()); for (int i = 0; i < 8; i++) { executor.submit(() -> System.out.println(Thread.currentThread().getName() + " is running")); }- DiscardOldestPolicy:丢弃队列中最老的任务(即队列头部的任务),然后尝试提交新任务。例如:
当队列满且有新任务时,会丢弃队列头部的任务,为新任务腾出空间。ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 4, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.DiscardOldestPolicy()); for (int i = 0; i < 8; i++) { executor.submit(() -> System.out.println(Thread.currentThread().getName() + " is running")); }- CallerRunsPolicy:当任务无法提交到线程池时,由提交任务的线程(调用者线程)来执行该任务。例如:
这样当线程池无法处理任务时,调用ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 4, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.CallerRunsPolicy()); for (int i = 0; i < 8; i++) { executor.submit(() -> System.out.println(Thread.currentThread().getName() + " is running")); }submit方法的主线程会执行任务。 - AbortPolicy:这是默认的拒绝策略,当任务无法提交到线程池(队列已满且线程数达到最大线程数)时,直接抛出
- Spring 中 Bean 的作用域:
- singleton(单例):在整个 Spring 容器中,只会创建一个该 Bean 的实例。例如,配置一个
UserService的 Bean 为单例,无论在多少个地方注入UserService,都是同一个实例。这适用于无状态的服务,如UserService只提供一些业务逻辑方法,不依赖于特定的用户状态等。 - prototype(原型):每次从容器中获取该 Bean 时,都会创建一个新的实例。比如
User类,不同的用户可能有不同的属性,每次获取UserBean 都希望是一个新的实例,就可以将其作用域设置为prototype。 - request:在一次 HTTP 请求中,只会创建一个该 Bean 的实例。当请求结束,该 Bean 实例也会被销毁。适用于处理与请求相关的业务逻辑,如
RequestContextHolder相关的操作,在每个请求中可能需要不同的上下文实例。 - session:在一个 HTTP Session 中,只会创建一个该 Bean 的实例。当 Session 过期或被销毁时,该 Bean 实例也会被销毁。常用于与用户会话相关的业务,比如用户登录后,在整个会话期间可能需要一个
UserSessionBean 来保存用户的会话信息。
- singleton(单例):在整个 Spring 容器中,只会创建一个该 Bean 的实例。例如,配置一个
- Spring Boot 自动配置原理:
- Spring Boot 依赖于
SpringFactoriesLoader机制。在META - INF/spring.factories文件中,定义了各种自动配置类。例如,spring - boot - starter - web依赖中,spring.factories文件里有WebMvcAutoConfiguration等自动配置类。 - 当 Spring Boot 应用启动时,
SpringFactoriesLoader会加载这些自动配置类。自动配置类通过@Configuration注解标记为配置类,并且使用@Conditional系列注解来判断是否满足自动配置的条件。比如@ConditionalOnClass表示当类路径下存在某个类时才进行配置,@ConditionalOnProperty表示当配置文件中某个属性满足条件时才进行配置。 - 以
DataSourceAutoConfiguration为例,@ConditionalOnClass(HikariDataSource.class)表示当类路径下存在HikariDataSource类时(即引入了 HikariCP 连接池依赖),才会进行数据源的自动配置。然后在配置类中,通过@Bean注解定义各种 Bean,如DataSourceBean,这样 Spring Boot 就自动帮我们配置好了数据源相关的内容。
- Spring Boot 依赖于
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译处理,MyBatis 在处理
#{}时,会将 SQL 中的#{}替换为?,然后使用PreparedStatement进行参数设置。例如,SQL 语句SELECT * FROM user WHERE username = #{username},实际执行时会变成SELECT * FROM user WHERE username =?,然后通过PreparedStatement.setString(1, "admin")这样的方式设置参数。这种方式能有效防止 SQL 注入,因为参数是作为字符串传入的,不会被解析为 SQL 语句的一部分。 - **{}
时,会直接将{username},如果username的值为admin,实际执行的 SQL 就是SELECT * FROM user WHERE username = admin。这种方式存在 SQL 注入风险,如果username的值被恶意修改为admin OR 1 = 1,就会导致 SQL 语句被篡改,查询出所有用户数据。所以在使用${}` 时要特别小心,一般用于传入表名、字段名等不会引起 SQL 注入的场景。
- #{}:是预编译处理,MyBatis 在处理
- Dubbo 服务暴露过程:
- 配置阶段:在服务提供方,通过 XML 配置或注解等方式定义要暴露的服务接口和实现类,以及注册中心地址等信息。例如,使用 XML 配置:
<dubbo:service interface="com.example.UserService" ref="userServiceImpl"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
- 初始化阶段:Dubbo 框架启动时,会解析配置信息,创建服务实例,并根据注册中心地址创建与注册中心的连接。
- **代理生成