第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,在 JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:当 ArrayList 元素个数达到容量阈值时,会创建一个新的数组,新数组容量是原数组容量的 1.5 倍,然后将原数组的元素复制到新数组。 面试官:很好。HashMap 在 JDK1.7 和 JDK1.8 中解决哈希冲突的方式有什么不同? 王铁牛:JDK1.7 主要是链表法,JDK1.8 当链表长度大于 8 且数组容量大于 64 时,会将链表转换为红黑树来提高查找效率。
第二轮面试 面试官:接下来聊聊多线程和线程池。创建线程有哪些方式? 王铁牛:可以继承 Thread 类,或者实现 Runnable 接口,还有实现 Callable 接口结合 FutureTask 来创建。 面试官:嗯,回答得不错。那线程池的核心参数有哪些,分别有什么作用? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位、任务队列。核心线程数是线程池初始化的线程数,最大线程数是线程池能容纳的最大线程数,存活时间是线程在没有任务时存活的时间,时间单位就是存活时间的单位,任务队列用来存放等待执行的任务。 面试官:那线程池的拒绝策略有哪些? 王铁牛:有 AbortPolicy,直接抛出异常;CallerRunsPolicy,让调用者线程来执行任务;DiscardPolicy,直接丢弃任务;DiscardOldestPolicy,丢弃队列中最老的任务。
第三轮面试 面试官:再谈谈框架相关的。Spring 中 Bean 的生命周期是怎样的? 王铁牛:嗯……先实例化,然后进行属性注入,接着如果实现了 Aware 接口,会调用相应的方法,再进行初始化,最后使用,销毁的时候如果实现了 DisposableBean 接口,会调用 destroy 方法。 面试官:那 Spring Boot 自动配置的原理是什么? 王铁牛:呃……好像是通过 SpringFactoriesLoader 加载 META - INF/spring.factories 文件中的配置,然后根据条件注解来决定是否生效。 面试官:Dubbo 服务暴露和引用的过程是怎样的? 王铁牛:嗯……就是服务提供者把服务暴露出去,然后消费者引用,中间通过注册中心来发现服务,具体过程有点复杂,我不太好描述。
面试官:好的,今天的面试就到这里。你的基础知识掌握得还可以,但对于一些稍微复杂的框架原理理解得不够深入。我们后续会综合评估所有候选人,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。
问题答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构,它可以动态扩容,方便按索引快速访问元素。例如,在需要频繁按顺序访问元素的场景,如学生成绩列表,使用 ArrayList 很合适。
- HashMap:JDK1.7 底层是数组加链表,JDK1.8 在此基础上,当链表长度大于 8 且数组容量大于 64 时,链表会转换为红黑树。这样在哈希冲突较少时,通过数组和链表能快速定位和存储元素;哈希冲突较多时,红黑树能保证查找效率。比如在存储用户信息,以用户 ID 为键时,HashMap 能快速根据 ID 找到对应的用户信息。
- ArrayList 扩容操作:
- 当 ArrayList 中元素个数达到容量阈值(一般是当前容量的 0.75 倍)时,会进行扩容。
- 扩容时,会创建一个新的数组,新数组容量是原数组容量的 1.5 倍(原容量左移一位再加 1)。
- 然后通过 System.arraycopy 方法将原数组的元素复制到新数组。这是为了在保证一定性能的同时,避免频繁扩容带来的开销。
- HashMap 在 JDK1.7 和 JDK1.8 中解决哈希冲突的方式:
- JDK1.7:采用链表法解决哈希冲突,即当不同的键计算出相同的哈希值时,会在数组的同一个位置以链表的形式存储多个值。但在哈希冲突严重时,链表长度会很长,查找效率会降低。
- JDK1.8:在链表法基础上,当链表长度大于 8 且数组容量大于 64 时,将链表转换为红黑树。红黑树是一种自平衡的二叉查找树,能保证在最坏情况下,查找、插入和删除操作的时间复杂度为 O(log n),提高了哈希冲突较多时的查找效率。
- 创建线程的方式:
- 继承 Thread 类:通过继承 Thread 类,重写 run 方法,然后创建该类的实例并调用 start 方法启动线程。例如:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行");
}
}
MyThread thread = new MyThread();
thread.start();
- 实现 Runnable 接口:实现 Runnable 接口的 run 方法,然后将该实现类的实例作为参数传递给 Thread 类的构造函数,再调用 start 方法启动线程。例如:
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 类的构造函数启动线程。可以通过 FutureTask 的 get 方法获取线程执行的返回结果。例如:
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);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
- 线程池的核心参数及作用:
- 核心线程数(corePoolSize):线程池初始化时创建的线程数,这些线程会一直存活,即使没有任务执行,除非设置了 allowCoreThreadTimeOut 为 true。例如,在一个 Web 服务器中,核心线程数可以设置为能处理日常请求量的线程数量。
- 最大线程数(maximumPoolSize):线程池能容纳的最大线程数。当任务队列已满且活动线程数小于最大线程数时,线程池会创建新的线程来处理任务。比如在电商大促时,可能会有大量请求,此时线程池可能会创建更多线程直到最大线程数来处理这些请求。
- 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程存活的最长时间。超过这个时间,这些空闲线程会被销毁。例如,在一个任务量有波动的系统中,任务量减少后,多余的线程在存活时间后会被销毁,避免资源浪费。
- 时间单位(unit):存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)等。
- 任务队列(workQueue):用于存放等待执行的任务。常见的任务队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。不同的任务队列特性会影响线程池的性能和行为。比如 ArrayBlockingQueue 有界,能防止任务队列无限增长导致内存溢出;LinkedBlockingQueue 无界,可能会导致任务堆积,但能保证所有任务都被接收。
- 线程池的拒绝策略:
- AbortPolicy:这是默认的拒绝策略,当任务无法提交到线程池(队列已满且线程数达到最大线程数)时,直接抛出 RejectedExecutionException 异常。适用于需要立即知道任务是否被执行的场景,如一些关键业务操作,不允许任务丢失。
- CallerRunsPolicy:当任务被拒绝时,会将任务交给调用者线程来执行。这样可以降低新任务的提交速度,同时利用调用者线程来执行任务,不会丢弃任务。例如在一个简单的单线程调用线程池的场景中,调用者线程可以帮忙处理任务,避免任务丢失。
- DiscardPolicy:直接丢弃被拒绝的任务,不做任何处理。适用于对任务执行结果不关心,且任务量较大,允许部分任务丢失的场景,如一些日志记录任务。
- DiscardOldestPolicy:丢弃队列中最老的任务(队首任务),然后尝试将新任务加入队列。这种策略适用于希望优先处理新任务的场景,例如在实时数据处理系统中,新数据可能更重要,老数据可以适当丢弃。
- Spring 中 Bean 的生命周期:
- 实例化:通过构造函数创建 Bean 的实例。
- 属性注入:使用依赖注入(DI)的方式为 Bean 的属性赋值。
- Aware 接口回调:如果 Bean 实现了 Aware 系列接口,如 BeanFactoryAware、ApplicationContextAware 等,Spring 容器会调用相应的 set 方法,将 BeanFactory 或 ApplicationContext 等对象传递给 Bean,使 Bean 能够感知到 Spring 容器的相关信息。
- 初始化:如果 Bean 实现了 InitializingBean 接口,会调用其 afterPropertiesSet 方法;或者在配置文件中通过 init - method 属性指定的方法,进行初始化操作。这一步可以进行一些资源初始化、数据加载等操作。
- 使用:Bean 可以被应用程序使用,提供相应的功能。
- 销毁:当 Spring 容器关闭时,如果 Bean 实现了 DisposableBean 接口,会调用其 destroy 方法;或者在配置文件中通过 destroy - method 属性指定的方法,进行销毁操作。这一步可以进行资源释放等操作,如关闭数据库连接、释放文件句柄等。
- Spring Boot 自动配置的原理:
- SpringFactoriesLoader:Spring Boot 利用 SpringFactoriesLoader 机制,它会在类路径下的 META - INF/spring.factories 文件中查找配置。这个文件中定义了各种自动配置类,如 DataSourceAutoConfiguration(数据库连接池自动配置)等。
- 条件注解:自动配置类上使用了各种条件注解,如 @ConditionalOnClass、@ConditionalOnProperty 等。例如,@ConditionalOnClass 表示当类路径下存在某个类时,该自动配置类才生效;@ConditionalOnProperty 表示当配置文件中某个属性满足一定条件时,该自动配置类才生效。通过这些条件注解,Spring Boot 可以根据项目的依赖和配置情况,动态决定哪些自动配置类应该生效,从而实现自动配置功能。这样开发者只需要引入相关依赖,Spring Boot 就能自动配置好相应的组件,大大简化了开发过程。
- Dubbo 服务暴露和引用的过程:
- 服务暴露:
- 配置解析:服务提供者在启动时,会读取 Dubbo 的配置文件,解析出服务接口、实现类、协议、端口等信息。
- 代理创建:通过动态代理技术(如 Javassist 或 JDK 动态代理)创建服务实现类的代理对象,这个代理对象负责将方法调用转换为远程调用。
- 协议绑定:根据配置的协议(如 Dubbo 协议、HTTP 协议等),将代理对象绑定到指定的端口,监听客户端请求。例如,使用 Dubbo 协议时,会启动一个 Netty 服务器来处理网络请求。
- 注册服务:服务提供者将服务接口和地址等信息注册到注册中心(如 Zookeeper、Redis 等),以便服务消费者能够发现该服务。
- 服务引用:
- 订阅服务:服务消费者启动时,从注册中心订阅自己需要的服务,注册中心会将服务提供者的地址列表返回给消费者。
- 代理创建:消费者端同样通过动态代理创建一个代理对象,这个代理对象负责将本地方法调用转换为远程调用。
- 负载均衡:当消费者调用服务方法时,代理对象会根据配置的负载均衡策略(如随机、轮询、最少活跃调用数等)从注册中心返回的服务提供者地址列表中选择一个地址,发起远程调用。
- 远程调用:根据配置的协议,与服务提供者建立连接并发送请求,获取响应结果返回给调用者。
- 服务暴露: