第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:当元素个数达到容量阈值时,会创建一个新的更大的数组,然后把旧数组的元素复制到新数组。 面试官:很好。HashMap 在 JDK1.8 中引入红黑树的条件是什么? 王铁牛:当链表长度大于 8 且数组容量大于等于 64 时,链表会转化为红黑树。
第二轮面试 面试官:接下来聊聊多线程和线程池。线程池有哪些核心参数? 王铁牛:有核心线程数、最大线程数、存活时间、阻塞队列这些。 面试官:那线程池的工作流程是怎样的,比如提交一个任务时? 王铁牛:嗯……先看核心线程有没有满,没满就创建核心线程执行任务,满了就放阻塞队列,队列满了就看最大线程数,没达到就创建非核心线程执行,要是都满了就触发拒绝策略。 面试官:还行。那在高并发场景下,线程池的拒绝策略一般怎么选择? 王铁牛:呃……就选默认的那个 AbortPolicy 吧,直接抛异常。
第三轮面试 面试官:谈谈 Spring 和 Spring Boot。Spring 中 Bean 的生命周期是怎样的? 王铁牛:嗯……先实例化,然后属性注入,接着初始化,最后销毁。 面试官:那 Spring Boot 自动配置的原理是什么? 王铁牛:就是……有个自动配置类,能根据依赖自动配置一些东西。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,{} 是字符串替换,#{} 能防止 SQL 注入。 面试官:Dubbo 服务调用的流程是怎样的? 王铁牛:呃……就是服务提供者注册服务,消费者去调用,中间好像还有个注册中心啥的。 面试官:RabbitMQ 在高并发场景下如何保证消息不丢失? 王铁牛:嗯……设置持久化吧,消息和队列都持久化。 面试官:xxl - job 任务调度的原理是什么? 王铁牛:这个……不太清楚,好像是有个调度中心啥的。 面试官:Redis 缓存雪崩怎么解决? 王铁牛:呃……给缓存设置不同的过期时间。
面试总结:从整体面试情况来看,你在一些基础知识点上掌握得还不错,像 ArrayList、HashMap 的底层结构,线程池的核心参数等回答得比较准确。但在一些进阶和深入的问题上,比如 Spring Boot 自动配置原理、Dubbo 服务调用流程、xxl - job 任务调度原理等,回答得不是很清晰和全面。回去等通知吧,我们会综合评估所有面试者的情况,在合适的时间给你回复。
答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构,它可以动态扩容。数组的特点是可以根据索引快速访问元素,但在插入和删除元素时,如果不是在末尾操作,可能需要移动大量元素,时间复杂度较高。
- HashMap:JDK1.8 之前底层是数组加链表结构,数组的每个位置是一个链表头节点。当发生哈希冲突时,会将新元素添加到链表中。JDK1.8 引入了红黑树,当链表长度大于 8 且数组容量大于等于 64 时,链表会转化为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,查找、插入和删除的时间复杂度平均为 O(log n)。
- ArrayList 扩容操作:当 ArrayList 中的元素个数达到容量阈值(一般是当前容量的 0.75 倍)时,会进行扩容。扩容时,会创建一个新的更大的数组,新数组的容量一般是原数组容量的 1.5 倍。然后通过 System.arraycopy 方法将旧数组中的元素复制到新数组中。
- HashMap 引入红黑树的条件:当链表长度大于 8 且数组容量大于等于 64 时,链表会转化为红黑树。这是因为当链表长度过长时,查找效率会降低,退化为 O(n),而红黑树能保证平均 O(log n) 的查找效率。
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut 为 true。
- 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列已满且核心线程都在忙碌时,线程池会创建新的线程,直到线程数达到最大线程数。
- 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程的存活时间。即这些空闲线程在存活时间内没有新任务,就会被销毁。
- 阻塞队列(workQueue):用于存放等待执行的任务。常见的阻塞队列有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。
- 线程池工作流程:当提交一个任务时,线程池首先判断核心线程是否已满,如果未满,则创建核心线程执行任务;如果核心线程已满,则将任务放入阻塞队列;如果阻塞队列也已满,再判断当前线程数是否达到最大线程数,如果未达到,则创建非核心线程执行任务;如果线程数已达到最大线程数,就会触发拒绝策略。
- 线程池拒绝策略选择:
- AbortPolicy:默认的拒绝策略,直接抛出 RejectedExecutionException 异常。适用于需要立即知道任务是否被成功执行的场景。
- CallerRunsPolicy:将任务交给调用者线程来执行。这样可以降低新任务的提交速度,缓解线程池的压力。
- DiscardPolicy:直接丢弃任务,不做任何处理。适用于可以容忍任务丢失的场景。
- DiscardOldestPolicy:丢弃队列中最老的任务(即将被执行的任务),然后尝试提交新任务。
- Spring Bean 的生命周期:
- 实例化(Instantiation):通过构造函数创建 Bean 实例。
- 属性注入(Populate):为 Bean 的属性设置值。
- 初始化(Initialization):调用 Bean 的初始化方法,比如实现 InitializingBean 接口的 afterPropertiesSet 方法,或者自定义的 init - method 方法。
- 销毁(Destruction):当容器关闭时,调用 Bean 的销毁方法,比如实现 DisposableBean 接口的 destroy 方法,或者自定义的 destroy - method 方法。
- Spring Boot 自动配置原理:Spring Boot 基于条件配置(@Conditional)实现自动配置。它通过 spring - factories 文件(META - INF/spring.factories)加载所有的自动配置类。这些自动配置类会根据项目中引入的依赖和配置属性,在满足一定条件时自动配置相应的 Bean。例如,当项目中引入了 spring - data - jpa 依赖时,Spring Boot 会自动配置 JPA 相关的 Bean,如 EntityManagerFactory、JpaTransactionManager 等。
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译处理,MyBatis 会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行参数设置,能有效防止 SQL 注入。例如:select * from user where username = #{username}。
- **{} 中的内容替换到 SQL 中。例如:select * from user where username = '${username}'。如果参数值为 '1 or 1 = 1',就会导致 SQL 注入。
- Dubbo 服务调用流程:
- 服务注册:服务提供者启动时,将自己提供的服务注册到注册中心(如 Zookeeper)。
- 服务订阅:服务消费者启动时,从注册中心订阅自己需要的服务。注册中心会将服务提供者的地址信息返回给消费者。
- 服务调用:消费者根据获取到的服务提供者地址,通过网络通信(如 Netty)调用服务提供者的接口。在调用过程中,可能会涉及到负载均衡、容错等机制。
- RabbitMQ 保证消息不丢失:
- 生产者消息不丢失:生产者可以开启 confirm 模式,当消息成功到达 Broker 时,Broker 会返回确认信息给生产者。如果生产者未收到确认信息,可以进行消息重发。
- 消息在 Broker 中不丢失:将队列和消息都设置为持久化。队列持久化通过声明队列时设置 durable = true 实现;消息持久化通过设置 MessageProperties.PERSISTENT_TEXT_PLAIN 实现。这样即使 Broker 重启,队列和消息也不会丢失。
- 消费者消息不丢失:消费者采用手动确认机制(acknowledgeMode = manual),在处理完消息后,手动向 Broker 发送确认信息。如果消费者在处理消息过程中出现异常,未发送确认信息,Broker 会将消息重新投递给其他消费者。
- xxl - job 任务调度原理:xxl - job 有一个调度中心,负责管理任务的注册、调度和监控。任务执行器(Executor)启动时会向调度中心注册自己,并汇报自己能执行的任务。调度中心根据任务的调度规则(如 cron 表达式)触发任务调度,将任务推送给对应的任务执行器。任务执行器接收到任务后,执行具体的任务逻辑,并将执行结果返回给调度中心。调度中心可以根据执行结果进行相应的处理,如记录日志、报警等。
- Redis 缓存雪崩解决方法:
- 设置不同过期时间:避免大量缓存同时过期。可以在原有的过期时间基础上,加上一个随机值,使缓存过期时间分散。
- 使用互斥锁:在缓存失效时,通过互斥锁(如 Redis 的 SETNX 命令)保证只有一个线程去查询数据库并更新缓存,其他线程等待。当更新完缓存后,释放互斥锁,其他线程可以从缓存中获取数据。
- 二级缓存:使用两层缓存,第一层缓存设置较短的过期时间,第二层缓存设置较长的过期时间。当第一层缓存失效时,先从第二层缓存获取数据,同时异步更新第一层缓存。