第一轮面试 面试官:先来聊聊Java基础,ArrayList和HashMap在存储结构上有什么区别? 王铁牛:ArrayList是基于数组的,能顺序存储数据。HashMap是基于哈希表,通过键值对存储。 面试官:不错,回答得挺清楚。那HashMap在什么情况下会发生哈希冲突,怎么解决的? 王铁牛:呃……数据多了可能就冲突了吧,解决好像是用链表,后来链表长了转红黑树。 面试官:嗯,大致方向对。再问个多线程的,创建线程有几种方式? 王铁牛:有继承Thread类,还有实现Runnable接口,还有Callable接口。 面试官:回答得不错,基础还是挺扎实的。
第二轮面试 面试官:那我们深入点,JUC包下的线程池,ThreadPoolExecutor的几个核心参数分别有什么作用? 王铁牛:呃……有核心线程数,就是一开始创建的线程数量,还有最大线程数,好像是最多能创建那么多线程,还有队列,用来存任务的。 面试官:嗯,说的有点笼统。那线程池在处理任务时,任务提交的流程是怎样的? 王铁牛:先看核心线程有没有满,满了就放队列,队列满了就看能不能创建新线程到最大线程数,再满了就拒绝策略。 面试官:还行,能说出个大概。JVM的垃圾回收机制,常见的垃圾回收算法有哪些? 王铁牛:有标记清除,标记整理,还有复制算法。 面试官:嗯,基本的算法知道。
第三轮面试 面试官:Spring框架中,IOC和AOP的原理分别是什么? 王铁牛:IOC是控制反转,把对象创建和管理交给容器。AOP是面向切面编程,能在不修改代码情况下增加功能。 面试官:原理说的比较浅。那Spring Boot自动配置的原理是什么? 王铁牛:呃……好像是根据依赖自动配置一些东西,具体不太清楚。 面试官:Dubbo作为分布式框架,它的服务调用流程是怎样的? 王铁牛:嗯……就是服务提供者注册服务,消费者去调用,中间有注册中心啥的。 面试官:好,今天的面试差不多了。你对自己今天的表现感觉怎么样?从你的回答来看,基础部分掌握得还可以,但在一些深入的原理和流程方面,理解还不够透彻。我们后续会综合评估所有候选人,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。
问题答案
- ArrayList和HashMap在存储结构上有什么区别:
- ArrayList:基于动态数组实现,它按照顺序存储元素,在内存中是连续存储的。这种结构适合按顺序访问元素,例如通过索引快速获取元素。它的扩容机制是当元素数量超过当前容量时,会创建一个新的更大的数组,并将原数组的元素复制到新数组中。
- HashMap:基于哈希表实现,以键值对的形式存储数据。它通过对键进行哈希运算,确定元素在哈希表中的存储位置。哈希表由数组和链表(或红黑树)组成,当哈希冲突发生时(不同键的哈希值相同),会使用链表或红黑树来解决冲突。在Java 8中,当链表长度超过8且数组容量大于64时,链表会转换为红黑树,以提高查找效率。
- HashMap在什么情况下会发生哈希冲突,怎么解决的:
- 哈希冲突发生情况:当不同的键经过哈希函数计算得到相同的哈希值时,就会发生哈希冲突。由于哈希函数的设计不可能做到完全唯一,所以哈希冲突是不可避免的。例如,假设哈希函数是简单的取模运算,不同的键对数组长度取模后可能得到相同的结果。
- 解决方法:
- 链地址法:在Java 8之前,HashMap主要使用链地址法解决哈希冲突。即当发生冲突时,在数组的同一个位置以链表的形式存储多个键值对。这样,通过哈希值找到数组位置后,再遍历链表找到对应的键值对。
- 红黑树优化:Java 8引入了红黑树来优化哈希冲突的处理。当链表长度超过8且数组容量大于64时,链表会转换为红黑树。红黑树是一种自平衡的二叉查找树,相比于链表,它在查找、插入和删除操作上具有更好的时间复杂度(O(log n)),从而提高了HashMap在冲突较多时的性能。
- 创建线程有几种方式:
- 继承Thread类:创建一个类继承Thread类,重写run()方法,在run()方法中编写线程执行的逻辑。然后创建该类的实例,调用start()方法启动线程。例如:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行中");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
- 实现Runnable接口:创建一个类实现Runnable接口,实现run()方法。然后创建该类的实例,并将其作为参数传递给Thread类的构造函数,最后调用start()方法启动线程。例如:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行中");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
- 实现Callable接口:创建一个类实现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;
}
}
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
Integer result = futureTask.get();
System.out.println("线程返回结果: " + result);
}
}
- ThreadPoolExecutor的几个核心参数分别有什么作用:
- corePoolSize:核心线程数,线程池在初始化后会创建corePoolSize个线程等待接收任务。即使这些线程处于空闲状态,也不会被销毁(除非设置了allowCoreThreadTimeOut为true)。例如,设置corePoolSize为5,线程池启动后就会创建5个线程。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。当任务队列已满,且当前线程数小于maximumPoolSize时,线程池会创建新的线程来处理任务。但线程数不会超过这个最大值。例如,设置maximumPoolSize为10,当任务过多时,线程池最多创建10个线程。
- keepAliveTime:线程存活时间,当线程数大于核心线程数时,多余的空闲线程在等待新任务到来的时间超过keepAliveTime后,会被销毁。例如,设置keepAliveTime为10秒,当有多余的空闲线程等待新任务超过10秒,这些线程就会被销毁。
- unit:keepAliveTime的时间单位,如TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)等。
- workQueue:任务队列,用于存放等待执行的任务。常用的任务队列有ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)、SynchronousQueue(同步队列)等。例如,使用ArrayBlockingQueue(10)表示创建一个容量为10的有界队列。
- threadFactory:线程工厂,用于创建新线程。可以通过自定义线程工厂来设置线程的名称、优先级等属性。例如:
ThreadFactory threadFactory = Executors.defaultThreadFactory();
- handler:拒绝策略,当任务队列已满且线程数达到最大线程数时,新提交的任务会被拒绝,由拒绝策略来处理。常见的拒绝策略有AbortPolicy(抛出异常)、CallerRunsPolicy(由调用者线程处理任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交新任务)。例如:
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
- 线程池在处理任务时,任务提交的流程是怎样的:
- 当提交一个新任务到线程池时,首先会判断当前运行的线程数是否小于核心线程数(corePoolSize)。如果小于,会创建一个新的核心线程来处理该任务。
- 如果当前运行的线程数已经达到核心线程数,任务会被放入任务队列(workQueue)中等待执行。
- 如果任务队列已满,且当前运行的线程数小于最大线程数(maximumPoolSize),线程池会创建一个新的非核心线程来处理任务。
- 如果任务队列已满,且当前运行的线程数已经达到最大线程数,此时会根据设置的拒绝策略(handler)来处理新提交的任务。例如,如果是AbortPolicy,会抛出RejectedExecutionException异常;如果是CallerRunsPolicy,会由调用者线程来执行该任务。
- JVM的垃圾回收机制,常见的垃圾回收算法有哪些:
- 标记 - 清除算法:
- 原理:分为标记和清除两个阶段。首先标记出所有需要回收的对象,然后在标记完成后,统一回收所有被标记的对象。
- 缺点:会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
- 复制算法:
- 原理:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
- 优点:实现简单,运行高效,不会产生内存碎片。
- 缺点:内存使用率低,因为每次都只有一半的内存可用。
- 标记 - 整理算法:
- 原理:标记过程和标记 - 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 优点:解决了标记 - 清除算法产生内存碎片的问题,同时也避免了复制算法内存使用率低的问题。
- 标记 - 清除算法:
- Spring框架中,IOC和AOP的原理分别是什么:
- IOC(控制反转)原理:
- 概念:IOC是一种设计思想,将对象的创建和管理控制权从应用程序代码转移到Spring容器中。
- 实现方式:Spring容器通过读取配置文件(XML或注解)来创建和管理对象。例如,使用XML配置时,通过
<bean>标签定义对象的创建信息,包括类名、属性等。在Java代码中,可以通过ApplicationContext等容器接口获取对象。Spring容器使用反射机制来实例化对象,并根据配置信息注入对象的依赖。例如:
- IOC(控制反转)原理:
<bean id="userService" class="com.example.UserService">
<property name="userDao" ref="userDao"/>
</bean>
<bean id="userDao" class="com.example.UserDao"/>
在Java代码中:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) context.getBean("userService");
- AOP(面向切面编程)原理:
- 概念:AOP是一种编程范式,它将横切关注点(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,以提高代码的可维护性和可复用性。
- 实现方式:Spring AOP基于动态代理实现。如果目标对象实现了接口,Spring AOP会使用JDK动态代理;如果目标对象没有实现接口,Spring AOP会使用CGLIB代理。动态代理是在运行时创建代理对象,代理对象会在调用目标方法前后执行切面逻辑(如前置通知、后置通知等)。例如,定义一个切面类:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class LoggingAspect {
@Around("execution(* com.example.UserService.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("方法调用前记录日志");
Object result = joinPoint.proceed();
System.out.println("方法调用后记录日志");
return result;
}
}
- Spring Boot自动配置的原理是什么:
- 条件配置:Spring Boot基于条件配置(@Conditional)机制。在Spring Boot的启动过程中,会扫描所有的自动配置类(位于META - INF/spring.factories文件中定义的配置类)。每个自动配置类都有相应的条件注解,例如
@ConditionalOnClass表示当类路径下存在某个类时才生效,@ConditionalOnProperty表示当配置文件中某个属性满足条件时才生效等。 - 依赖管理:Spring Boot通过
spring - boot - starter - *依赖来管理项目的依赖。这些starter依赖包含了项目常用的依赖和自动配置类。例如,spring - boot - starter - web依赖包含了Spring Web相关的依赖和自动配置,使得引入该依赖后,Spring Web相关的功能(如Tomcat服务器、Spring MVC等)能自动配置并可用。 - 配置加载:Spring Boot会自动加载
application.properties或application.yml等配置文件中的属性,并将这些属性与自动配置类中的属性进行绑定。例如,配置文件中的server.port属性会被绑定到Tomcat服务器的端口配置上,从而实现服务器端口的自定义配置。
- 条件配置:Spring Boot基于条件配置(@Conditional)机制。在Spring Boot的启动过程中,会扫描所有的自动配置类(位于META - INF/spring.factories文件中定义的配置类)。每个自动配置类都有相应的条件注解,例如
- Dubbo作为分布式框架,它的服务调用流程是怎样的:
- 服务注册:
- 服务提供者启动时,会读取配置文件中定义的服务接口和实现类等信息。
- 通过Dubbo的注册中心(如Zookeeper、Redis等),将自己提供的服务注册到注册中心,注册信息包括服务接口、服务地址、版本号等。
- 服务订阅:
- 服务消费者启动时,会从配置文件中读取需要调用的服务接口信息。
- 向注册中心订阅自己所需的服务,注册中心会将服务提供者的地址列表返回给服务消费者。
- 服务调用:
- 服务消费者在调用服务时,会从注册中心返回的服务提供者地址列表中选择一个地址(可以通过负载均衡算法,如随机、轮询等)。
- 然后通过Dubbo的远程通信协议(如Dubbo协议、HTTP协议等)与服务提供者建立连接,并发起远程方法调用。
- 服务提供者接收到调用请求后,根据请求中的接口和方法信息,找到对应的实现类并执行方法,将执行结果返回给服务消费者。
- 服务注册: