第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,在 JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那 HashMap 在什么情况下会发生扩容? 王铁牛:当 HashMap 中的元素个数达到负载因子(默认 0.75)乘以当前容量的时候,就会进行扩容。 面试官:回答得很好。再问个多线程的基础问题,创建线程有几种方式? 王铁牛:有三种,继承 Thread 类、实现 Runnable 接口、还有实现 Callable 接口通过 FutureTask 包装。
第二轮面试 面试官:刚刚提到多线程,那说说线程池的核心参数有哪些,分别有什么作用? 王铁牛:呃……有核心线程数,好像是一直保留的线程数量,还有最大线程数,就是最多能创建的线程数,其他的……我不太确定了。 面试官:好,那说说 JVM 的内存区域划分,简单讲讲每个区域的作用。 王铁牛:有堆,是存放对象实例的,还有栈,好像是方法执行的地方,其他的我有点混淆了。 面试官:那 Spring 框架中,IOC 和 AOP 分别是什么,简单说说原理。 王铁牛:IOC 是控制反转,就是把对象创建和管理交给 Spring 容器。AOP 是面向切面编程,好像是能在方法前后加一些逻辑。
第三轮面试 面试官:在 Spring Boot 项目中,如何实现自定义的 Starter?讲讲大概步骤。 王铁牛:嗯……要先创建一个模块,然后写一些配置类,具体的细节我不太清楚了。 面试官:MyBatis 中,#{} 和 {} 的区别是什么,在实际业务场景中怎么选择? **王铁牛**:#{} 是预编译,{} 是字符串替换,#{} 能防止 SQL 注入,至于业务场景选择,我……不太会说。 面试官:Dubbo 服务调用过程中,如何进行服务治理,比如负载均衡策略有哪些? 王铁牛:有随机、轮询,其他的我一下子想不起来了。 面试官:RabbitMQ 如何保证消息的可靠性传递,讲讲思路。 王铁牛:好像是开启确认机制,其他的我不太确定。 面试官:最后问个 xxl - job 相关的,如何在项目中集成 xxl - job 实现分布式任务调度? 王铁牛:要引入依赖,然后配置一些东西,具体不太记得了。
面试结束,面试官总结道:“今天的面试就到这里,你对一些基础的知识点掌握得还可以,但对于一些稍微复杂和深入的问题,回答得不是特别清晰和全面。我们后续会综合评估所有面试者的情况,你回家等通知吧。如果有进一步的消息,我们会及时联系你。感谢你今天来参加面试。”
问题答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构。数组可以快速地根据索引访问元素,所以 ArrayList 支持快速的随机访问。当添加元素时,如果数组容量不足,会进行扩容,一般是扩容为原来的 1.5 倍。
- HashMap:在 JDK1.7 及以前,底层是数组加链表结构。数组的每个位置是一个链表的头节点。当发生哈希冲突(不同的 key 计算出相同的哈希值)时,会将新的 key - value 对以链表的形式挂在对应数组位置的链表上。JDK1.8 后,当链表长度大于 8 且数组容量大于 64 时,链表会转化为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,能保证在最坏情况下的查找时间复杂度为 O(log n)。
- HashMap 扩容机制:HashMap 有两个重要的参数,容量(capacity)和负载因子(loadFactor)。负载因子默认是 0.75。当 HashMap 中的元素个数(size)达到负载因子乘以当前容量(即 size >= capacity * loadFactor)时,就会触发扩容。扩容时,会创建一个新的更大的数组,然后将旧数组中的所有元素重新计算哈希值并放入新数组中。新容量一般是旧容量的 2 倍。
- 创建线程的方式:
- 继承 Thread 类:通过继承 Thread 类,重写 run 方法,在 run 方法中编写线程执行的逻辑。例如:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行逻辑");
}
}
MyThread myThread = new MyThread();
myThread.start();
- 实现 Runnable 接口:实现 Runnable 接口,重写 run 方法,然后将实现类的实例作为参数传递给 Thread 类的构造函数。例如:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行逻辑");
}
}
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
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<Integer> {
@Override
public Integer call() throws Exception {
return 100;
}
}
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
try {
Integer result = futureTask.get();
System.out.println("线程返回结果:" + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池中一直存活的线程数量,即使这些线程处于空闲状态,也不会被销毁。当有新任务提交时,如果当前线程数小于核心线程数,会创建新的线程来处理任务。
- 最大线程数(maximumPoolSize):线程池中允许创建的最大线程数。当任务队列已满,且当前线程数小于最大线程数时,会创建新的线程来处理任务。
- 队列容量(workQueue):用于存放等待执行的任务的队列。常用的队列类型有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列,默认容量为 Integer.MAX_VALUE)等。
- 线程存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程在多长时间后会被销毁。
- 时间单位(unit):线程存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
- JVM 内存区域划分:
- 堆(Heap):是 JVM 中最大的一块内存区域,用于存放对象实例。堆可以分为新生代和老年代,新生代又分为 Eden 区和两个 Survivor 区(一般是 S0 和 S1)。新创建的对象一般先放在 Eden 区,当 Eden 区满了,会触发 Minor GC,存活的对象会被移动到 Survivor 区,在 Survivor 区经过多次 GC 后,对象会晋升到老年代。
- 栈(Stack):每个线程都有自己的栈,用于存放方法调用的局部变量、操作数栈、动态链接等信息。栈是线程私有的,随着线程的创建而创建,随着线程的结束而销毁。当一个方法被调用时,会在栈中创建一个栈帧,方法执行完毕后,栈帧会被弹出栈。
- 方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK8 及以后,方法区被元空间(Metaspace)取代,元空间使用本地内存。
- 程序计数器(Program Counter Register):也是线程私有的,它记录的是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 本地方法栈(Native Method Stack):与 Java 栈类似,只不过它是为 JVM 使用到的本地(Native)方法服务的。
- Spring 中 IOC 和 AOP:
- IOC(控制反转):原理是通过一个容器(如 ApplicationContext)来管理对象的创建、配置和生命周期。传统方式是在代码中自己创建对象,而 IOC 是把对象的创建和管理交给 Spring 容器。Spring 容器通过读取配置文件(如 XML 配置或注解配置)来实例化对象,并注入对象之间的依赖关系。例如,在 XML 配置中,可以通过 标签定义一个对象,Spring 容器会根据配置创建该对象。在注解配置中,可以使用 @Component、@Service 等注解标记一个类,Spring 容器会自动扫描并创建对象。
- AOP(面向切面编程):AOP 是将一些通用的功能(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,形成一个个切面(Aspect)。在 Spring 中,通过动态代理(JDK 动态代理或 CGLIB 代理)来实现 AOP。当目标方法被调用时,会先执行切面中定义的前置通知(Before Advice),然后执行目标方法,再执行后置通知(After Advice)等。例如,通过 @Aspect 注解定义一个切面类,在切面类中定义各种通知方法。
- Spring Boot 自定义 Starter:
- 创建一个 Maven 项目:作为自定义 Starter 的模块。
- 定义自动配置类:在 resources/META - INF/spring.factories 文件中配置自动配置类。自动配置类使用 @Configuration 注解标记,在类中可以使用 @Bean 注解定义需要注入到 Spring 容器中的 bean。例如,定义一个自定义的服务类,并在自动配置类中创建该服务类的 bean。
- 定义 Starter 的依赖:在 pom.xml 文件中定义 Starter 所依赖的其他库。例如,如果自定义 Starter 依赖于数据库连接,就需要引入相关的数据库连接依赖。
- 发布 Starter:可以将自定义 Starter 发布到本地 Maven 仓库或远程 Maven 仓库,供其他项目使用。其他项目在引入该 Starter 依赖后,Spring Boot 会自动加载相关的自动配置。
- MyBatis 中 #{} 和 ${} 的区别及业务场景选择:
- 区别:
- #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行设置参数值,这样可以有效防止 SQL 注入。例如:
SELECT * FROM user WHERE username = #{username},最终执行的 SQL 是SELECT * FROM user WHERE username =?,然后通过 PreparedStatement 设置参数值。 - **{} 时,会直接将 {username}
,如果 username 的值是 'admin',最终执行的 SQL 就是SELECT * FROM user WHERE username = admin`,这种方式容易导致 SQL 注入。
- #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行设置参数值,这样可以有效防止 SQL 注入。例如:
- 业务场景选择:
- #{}:在大多数情况下,尤其是涉及用户输入的参数,都应该使用 #{},以保证 SQL 的安全性。例如,登录功能中,用户名和密码的查询参数。
- **{}。但使用时要确保传入的值是经过严格校验的。
- 区别:
- Dubbo 服务治理之负载均衡策略:
- 随机(Random):随机选择一个服务提供者。算法简单,在服务提供者性能相近的情况下,能比较均匀地分配请求。例如,假设有三个服务提供者 A、B、C,每次请求时,随机从这三个中选择一个。
- 轮询(RoundRobin):按照顺序依次选择服务提供者。例如,第一次请求选择 A,第二次选择 B,第三次选择 C,第四次又选择 A,以此类推。在服务提供者性能相近时,能均匀分配请求,但如果某个服务提供者性能较差,可能会导致该服务提供者压力过大。
- 最少活跃调用数(LeastActive):优先选择活跃调用数最少的服务提供者。活跃调用数表示当前正在处理的请求数量,选择活跃调用数最少的服务提供者,可以将请求分配到负载较轻的节点上,提高整体的处理效率。
- 一致性哈希(ConsistentHash):根据请求的参数计算出一个哈希值,然后将哈希值映射到一个哈希环上,选择距离该哈希值最近的服务提供者。这种方式在服务提供者数量变化时,能尽量减少请求的重新分配,适用于对特定参数有粘性要求的场景,例如,某个用户的请求始终路由到同一个服务提供者,以保证数据的一致性。
- RabbitMQ 保证消息可靠性传递:
- 开启确认机制(publisher confirm):生产者将信道设置为 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上发布的消息都会被指派一个唯一的 ID。当消息被投递到所有匹配的队列后,RabbitMQ 会发送一个确认(Basic.Ack)给生产者,包含消息的唯一 ID。如果消息和队列是持久化的,那么确认消息会在消息被写入磁盘后发出。如果 RabbitMQ 因为某些原因(如队列满、消息过期等)未能将消息成功投递到队列,会发送一个 nack(Basic.Nack)消息给生产者,生产者可以根据这个进行相应的处理,如重新发送消息。
- 事务机制:生产者可以通过设置信道为事务模式(txSelect),然后在发送消息前开启事务(txBegin),发送消息后提交事务(txCommit)。如果提交事务失败,RabbitMQ 会回滚事务,消息不会被投递到队列。但事务机制会严重影响性能,因为它是同步阻塞的,在事务提交前,生产者无法发送其他消息。
- 消息持久化:包括队列持久化和消息持久化。队列持久化通过声明队列时设置 durable 为 true 来实现,这样 RabbitMQ 重启后队列依然存在。消息持久化通过设置消息的 deliveryMode 为 2 来实现,这样消息会被写入磁盘,即使 RabbitMQ 重启,消息也不会丢失。
- xxl - job 集成实现分布式任务调度:
- 引入依赖:在项目的 pom.xml 文件中引入 xxl - job 相关的依赖,例如:
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl - job - core</artifactId>
<version>2.3.0</version>
</dependency>
- 配置调度中心:在 application.properties 或 application.yml 文件中配置 xxl - job 调度中心的地址、访问令牌等信息。例如:
xxl.job.admin.addresses = http://127.0.0.1:8080/xxl - job - admin
xxl.job.accessToken =
- 创建任务执行器:创建一个配置类,使用 @Configuration 和 @EnableXxlJob 注解开启 xxl - job 任务执行器功能。例如:
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class XxlJobConfig {
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses("http://127.0.0.1:8080/xxl - job - admin");
xxlJobSpringExecutor.setAccessToken("");
xxlJobSpringExecutor.setAppname("xxl - job - demo - executor");
xxlJobSpringExecutor.setAddress("");
xxlJobSpringExecutor.setIp("");
xxlJobSpringExecutor.setPort(9999);
xxlJobSpringExecutor.setLogPath("/data/applogs/xxl - job - executor");
xxlJobSpringExecutor.setLogRetentionDays(30);
return xxlJobSpringExecutor;
}
}
- 编写任务类:创建一个类,使用 @XxlJob 注解标记任务方法,在方法中编写具体的任务逻辑。例如:
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;
@Component
public class MyXxlJob {
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
System.out.println("执行任务逻辑");
}
}
- 在调度中心配置任务:在 xxl - job 调度中心的界面上,配置任务的执行器、任务名称、