第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,后来 JDK 1.8 之后引入了红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:嗯……好像是当元素个数达到容量的一定比例,就会扩容,新容量大概是原来的 1.5 倍,然后把旧数组的数据复制到新数组。 面试官:回答得可以。HashMap 在 JDK 1.8 中引入红黑树,为什么要引入呢? 王铁牛:呃,好像是为了优化查找性能吧,链表太长查找就慢,红黑树能让查找快一些。 面试官:好,第一轮表现不错。
第二轮面试 面试官:接下来聊聊多线程和线程池。多线程编程中,线程的生命周期有哪些状态? 王铁牛:新建、就绪、运行、阻塞、死亡这几个状态。 面试官:很好。那线程池的核心参数有哪些,分别有什么作用? 王铁牛:有核心线程数、最大线程数,核心线程数就是一直保留的线程数量,最大线程数是能创建的最多线程数。还有队列容量,用来存放等待执行的任务。 面试官:还行。那在高并发场景下,线程池拒绝策略有哪些,分别适用于什么场景? 王铁牛:呃……有 AbortPolicy,直接抛出异常,这个好像适用于不能丢弃任务的场景。其他的……我不太记得了。 面试官:第二轮整体还行,有些点还需要加强。
第三轮面试 面试官:我们谈谈框架相关的,Spring 框架中 IOC 和 AOP 分别是什么,有什么作用? 王铁牛:IOC 是控制反转,就是把对象的创建和管理交给 Spring 容器。AOP 是面向切面编程,能在不修改原有代码的情况下,增加一些通用功能,像日志记录、事务管理。 面试官:回答得不错。那 Spring Boot 相对于 Spring 有什么优势,它是如何实现快速开发的? 王铁牛:Spring Boot 简化了配置,很多配置都有默认值,能快速搭建项目。它内置了服务器,像 Tomcat 这些,不用再单独配置。 面试官:还可以。那 MyBatis 框架中,#{} 和 {} 的区别是什么,在实际开发中怎么选择? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 是直接替换,在需要拼接 SQL 语句的时候用 ${},但要注意 SQL 注入风险。 面试官:最后问个分布式相关的,Dubbo 框架的工作原理是什么,在微服务架构中有什么作用? 王铁牛:嗯……Dubbo 好像是进行服务治理的,能实现服务的注册与发现,还有负载均衡。具体原理……我不太清楚了。 面试官:王铁牛,整体来看,基础部分掌握得还可以,但一些进阶和深入的知识点理解得不够透彻。回去等通知吧,我们会综合评估所有候选人后,再做决定。
问题答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构。数组可以快速地根据索引访问元素,适合频繁的随机访问操作。例如,当我们需要根据下标快速获取某个元素时,数组的特性就能很好地满足需求。
- HashMap:JDK 1.8 之前底层是数组加链表结构。数组的每个位置是一个链表的头节点,通过哈希算法计算 key 的哈希值,再对数组长度取模确定元素在数组中的位置。如果发生哈希冲突(不同 key 计算出相同的数组位置),就以链表的形式存储在该位置。JDK 1.8 之后引入红黑树,当链表长度大于 8 且数组容量大于 64 时,链表会转换为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,能保证在最坏情况下,查找、插入和删除操作的时间复杂度为 O(log n),相比链表的 O(n) 查找效率有很大提升。
- ArrayList 扩容操作:
- ArrayList 有一个容量的概念,当向 ArrayList 中添加元素时,如果当前元素个数达到了容量大小,就会触发扩容。
- 扩容时,新容量是原来容量的 1.5 倍(通过位运算实现,
newCapacity = oldCapacity + (oldCapacity >> 1))。 - 然后创建一个新的更大的数组,将旧数组中的元素复制到新数组中。这个复制过程使用
System.arraycopy方法,它是一个 native 方法,效率较高。例如:
Object[] oldData = elementData;
int oldCapacity = oldData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
Object[] newData = new Object[newCapacity];
System.arraycopy(oldData, 0, newData, 0, Math.min(oldCapacity, size));
elementData = newData;
- HashMap 引入红黑树的原因:
- 在 JDK 1.8 之前,HashMap 处理哈希冲突是通过链表。当哈希冲突严重时,链表会变得很长,此时查找元素的时间复杂度会退化为 O(n),性能急剧下降。
- 引入红黑树后,当链表长度大于 8 且数组容量大于 64 时,链表转换为红黑树。红黑树能保证在最坏情况下,查找、插入和删除操作的时间复杂度为 O(log n),大大提高了在哈希冲突较多情况下的查找性能。例如,在存储大量数据且哈希分布不均匀时,红黑树能有效避免链表过长导致的性能问题。
- 线程的生命周期状态:
- 新建(New):当创建一个 Thread 对象,但还未调用
start()方法时,线程处于新建状态。此时线程还没有开始运行,只是一个空的线程对象。例如:Thread thread = new Thread(); - 就绪(Runnable):调用
start()方法后,线程进入就绪状态。此时线程已经准备好运行,但还没有获得 CPU 时间片,等待 CPU 调度。 - 运行(Running):当线程获得 CPU 时间片后,开始执行
run()方法中的代码,此时线程处于运行状态。 - 阻塞(Blocked):线程在运行过程中,可能会因为某些原因进入阻塞状态,例如调用
sleep()方法、等待锁、进行 I/O 操作等。在阻塞状态下,线程不会获得 CPU 时间片,直到阻塞条件解除。例如,线程调用Thread.sleep(1000);后,会进入阻塞状态 1 秒。 - 死亡(Terminated):当
run()方法执行完毕或者因为异常退出时,线程进入死亡状态。此时线程的生命周期结束,不能再重新启动。
- 新建(New):当创建一个 Thread 对象,但还未调用
- 线程池的核心参数及作用:
- 核心线程数(corePoolSize):线程池中一直存活的线程数量,即使这些线程处于空闲状态,也不会被销毁。当有新任务提交时,如果当前线程数小于核心线程数,会优先创建新的核心线程来处理任务。例如,核心线程数设置为 5,那么线程池启动后,会立即创建 5 个线程等待处理任务。
- 最大线程数(maximumPoolSize):线程池中允许创建的最大线程数量。当任务队列已满,且当前线程数小于最大线程数时,会创建新的非核心线程来处理任务。但要注意,如果任务过多,创建过多的线程可能会导致系统资源耗尽。例如,最大线程数设置为 10,当核心线程数 5 个都在忙碌且任务队列已满时,会继续创建线程,直到线程数达到 10 个。
- 队列容量(workQueue):用于存放等待执行的任务的队列。当核心线程都在忙碌时,新提交的任务会被放入队列中等待。常见的队列类型有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)等。例如,使用 ArrayBlockingQueue(10) 创建一个容量为 10 的队列,最多可以存放 10 个等待执行的任务。
- 线程存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程在多长时间后会被销毁。例如,设置存活时间为 60 秒,那么当有非核心线程空闲 60 秒后,会被销毁。
- 时间单位(unit):线程存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
- 线程池拒绝策略及适用场景:
- AbortPolicy:直接抛出 RejectedExecutionException 异常。适用于不能丢弃任务,并且任务队列已满,线程池也达到最大线程数的场景。例如,在一些关键业务场景中,任务必须被处理,不允许丢弃,此时可以使用该策略,让调用者能够捕获异常并进行相应处理。
- CallerRunsPolicy:将被拒绝的任务交给调用者线程来执行。适用于任务提交速度过快,超过线程池处理能力的场景。这样可以降低任务提交的速度,因为调用者线程在执行任务时,会阻塞后续任务的提交。例如,在一个 Web 应用中,用户请求过多导致线程池处理不过来,使用该策略可以让请求线程自己处理任务,避免大量请求堆积。
- DiscardPolicy:直接丢弃被拒绝的任务,不做任何处理。适用于可以丢弃不重要任务的场景,例如一些日志记录任务,如果线程池繁忙,丢弃一些日志记录任务对系统影响不大。
- DiscardOldestPolicy:丢弃队列中最老的任务(即将队列头部的任务丢弃),然后尝试将新任务加入队列。适用于希望优先处理新任务,并且任务队列有一定容量限制的场景。例如,在一些实时数据处理场景中,新数据更重要,老数据可以适当丢弃。
- Spring 框架中 IOC 和 AOP:
- IOC(控制反转):
- 概念:将对象的创建和管理控制权从应用程序代码转移到 Spring 容器中。在传统编程中,对象的创建和依赖关系的管理由应用程序自己负责,代码耦合度高。而在 Spring 中,通过 IOC 容器,对象的创建、配置和组装都由容器来完成。
- 作用:降低代码的耦合度,提高代码的可维护性和可测试性。例如,一个类 A 依赖类 B,如果在类 A 中直接创建类 B 的实例,那么类 A 与类 B 紧密耦合。使用 IOC 后,类 A 只需要声明对类 B 的依赖,由 Spring 容器来创建和注入类 B 的实例,这样类 A 不需要关心类 B 的具体创建过程,当类 B 发生变化时,对类 A 的影响较小。
- AOP(面向切面编程):
- 概念:将一些通用的功能(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,以切面的形式织入到业务逻辑中。这些通用功能被称为切面,业务逻辑被称为切点。
- 作用:提高代码的复用性和可维护性。例如,在多个业务方法中都需要进行日志记录,如果在每个业务方法中都编写日志记录代码,会导致代码重复。使用 AOP 可以将日志记录功能封装成一个切面,然后通过配置将其织入到需要的业务方法中,这样既实现了功能,又不会污染业务逻辑代码。
- IOC(控制反转):
- Spring Boot 相对于 Spring 的优势及快速开发实现方式:
- 优势:
- 简化配置:Spring Boot 提供了大量的默认配置,减少了开发人员手动配置的工作量。例如,在使用 Spring 开发 Web 应用时,需要手动配置 Tomcat 服务器、Spring MVC 等相关配置,而 Spring Boot 只需要引入相关依赖,就能自动配置好这些内容。
- 快速搭建项目:通过 Spring Initializr 可以快速生成一个基于 Spring Boot 的项目骨架,包含了项目所需的基本依赖和目录结构,开发人员可以直接在这个基础上进行业务开发。
- 内置服务器:Spring Boot 内置了 Tomcat、Jetty 等服务器,不需要再单独安装和配置服务器,直接运行项目即可启动服务器。例如,使用
mvn spring - boot:run命令就能启动一个包含内置 Tomcat 服务器的 Spring Boot 项目。
- 快速开发实现方式:
- 依赖管理:Spring Boot 使用 Maven 或 Gradle 进行依赖管理,通过
spring - boot - starter - *系列依赖,能方便地引入各种功能模块。例如,spring - boot - starter - web依赖可以快速引入 Spring MVC 和 Tomcat 等 Web 开发相关的依赖。 - 自动配置:Spring Boot 根据项目引入的依赖,自动进行相关的配置。例如,引入
spring - boot - starter - data - jpa依赖后,Spring Boot 会自动配置好 JPA 相关的数据源、实体管理器等。 - Actuator 监控:Spring Boot Actuator 提供了对应用程序的监控和管理功能,能方便地查看应用程序的运行状态、性能指标等,有助于快速定位和解决问题。
- 依赖管理:Spring Boot 使用 Maven 或 Gradle 进行依赖管理,通过
- 优势:
- MyBatis 框架中 #{} 和 ${} 的区别及选择:
- 区别:
- #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行参数设置。这种方式能有效防止 SQL 注入攻击,因为参数是作为字符串传入的,不会被解析为 SQL 语句的一部分。例如:
SELECT * FROM user WHERE username = #{username},实际执行的 SQL 是SELECT * FROM user WHERE username =?,然后通过 PreparedStatement 设置参数值。 - **{} 时,会直接将 {username}'
,如果username参数被恶意设置为' OR '1' = '1,那么实际执行的 SQL 就变成了SELECT * FROM user WHERE username = '' OR '1' = '1'`,会导致查询出所有用户数据。
- #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行参数设置。这种方式能有效防止 SQL 注入攻击,因为参数是作为字符串传入的,不会被解析为 SQL 语句的一部分。例如:
- 选择:
- 在需要防止 SQL 注入的场景下,优先使用 #{}。例如,在处理用户输入的查询条件时,如登录验证、搜索功能等,都应该使用 #{}。
- 当需要进行 SQL 语句的动态拼接,且参数值是固定的、安全的情况下,可以使用 {}。例如:
SELECT * FROM ${tableName},这里的tableName是在代码中固定设置的,不会有 SQL 注入风险。
- 区别:
- Dubbo 框架的工作原理及在微服务架构中的作用:
- 工作原理:
- 服务注册与发现:Dubbo 采用注册中心(如 Zookeeper)来实现服务的注册与发现。服务提供者启动时,会将自己提供的服务注册到注册中心,注册中心保存服务的元数据信息(如服务接口、地址、端口等)。服务消费者启动时,会从注册中心订阅自己需要的服务,注册中心会将服务提供者的地址信息返回给服务消费者。
- 远程调用:服务消费者通过代理对象调用服务提供者的方法,Dubbo 会将方法调用转换为远程调用。它支持多种通信协议,如 Dubbo 协议、HTTP 协议等。在远程调用过程中,Dubbo 会进行序列化和反序列化操作,将方法参数和返回值进行转换,以便在网络中传输。
- 负载均衡:当有多个服务提供者提供相同的服务时,Dubbo 会使用负载均衡算法(如随机、轮询、权重等)选择一个服务提供者进行调用,以实现负载均衡,提高系统的可用性和性能。
- 在微服务架构中的作用:
- 服务治理:Dubbo 提供了服务的注册与发现、负载均衡、服务监控等功能,方便对微服务进行管理和治理。例如,通过服务监控可以了解每个服务的调用次数、响应时间等指标,以便及时发现和解决问题。
- 提高系统可扩展性:在微服务架构中,系统由多个独立的微服务组成,Dubbo 可以帮助各个微服务之间进行高效的通信和协作。当业务规模扩大时,可以方便地增加新的服务提供者,Dubbo 的负载均衡功能能自动将请求分配到新的服务提供者上,提高系统的可扩展性。
- 降低系统耦合度:Dubbo 采用接口化的方式进行服务调用,服务提供者和服务消费者只需要关注接口,而不需要关心对方的具体实现。这样可以降低微服务之间的耦合度,使得各个微服务可以独立开发、部署和维护。