第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:当 ArrayList 元素个数达到容量阈值时,会创建一个新的数组,新数组容量是原数组的 1.5 倍,然后把原数组的元素复制到新数组。 面试官:很好。HashMap 在 JDK1.8 中,链表转红黑树的条件是什么? 王铁牛:当链表长度大于等于 8 且数组长度大于等于 64 时,链表会转成红黑树。
第二轮面试 面试官:接下来聊聊多线程和线程池。线程有哪些状态? 王铁牛:新建、就绪、运行、阻塞、死亡这几种状态。 面试官:那线程池的核心参数有哪些,分别代表什么含义? 王铁牛:有核心线程数,就是线程池里常驻的线程数;最大线程数,线程池能容纳的最大线程数;还有阻塞队列,用来存放任务的队列。 面试官:那线程池是如何根据这些参数来处理任务的? 王铁牛:嗯……就是先看核心线程有没有满,没满就创建核心线程处理任务,满了就放队列里,队列满了再看能不能创建非核心线程,要是达到最大线程数还处理不过来就触发拒绝策略。
第三轮面试 面试官:再谈谈框架相关的,Spring 中 Bean 的生命周期了解吗? 王铁牛:嗯,就是从实例化、属性赋值、初始化到销毁的过程。 面试官:Spring Boot 自动配置的原理是什么? 王铁牛:呃……好像是通过一些配置类和条件注解,自动帮我们配置一些组件。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 能防止 SQL 注入。 面试官:Dubbo 服务调用的流程是怎样的? 王铁牛:嗯……就是服务提供者注册服务,消费者从注册中心获取服务列表,然后进行调用。 面试官:RabbitMQ 有哪些常见的应用场景? 王铁牛:可以用于异步处理、解耦系统、流量削峰。 面试官:xxl - job 任务调度的原理是什么? 王铁牛:呃……不太清楚。 面试官:Redis 缓存雪崩、缓存穿透、缓存击穿分别是什么,怎么解决? 王铁牛:缓存雪崩好像是大量缓存同时过期,缓存穿透是查询不存在的数据一直穿透到数据库,缓存击穿是一个热点 key 过期瞬间大量请求打到数据库。解决办法……我不太记得全了。
面试总结:从这三轮面试来看,你在一些基础知识的掌握上表现不错,像 ArrayList、HashMap 的底层结构,线程的基本状态等回答得都很准确。但在一些稍微深入和复杂的问题上,比如 Spring Boot 自动配置原理、xxl - job 任务调度原理以及 Redis 相关问题的解决方案,回答得不是很清晰和全面。我们会综合考虑所有面试者的情况,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。
答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构,它可以动态扩容,方便按索引快速访问元素。
- HashMap:JDK1.8 之前底层是数组加链表,JDK1.8 引入红黑树后,当链表长度大于等于 8 且数组长度大于等于 64 时,链表会转成红黑树,这样在哈希冲突严重时能提高查找效率。数组用于存储哈希桶,链表或红黑树挂在哈希桶上解决哈希冲突。
- ArrayList 扩容操作:当 ArrayList 元素个数达到容量阈值(一般是当前容量的 0.75 倍)时,会创建一个新的数组,新数组容量是原数组的 1.5 倍(原容量左移一位再加 1)。然后通过 System.arraycopy 方法把原数组的元素复制到新数组。
- HashMap 链表转红黑树条件:当链表长度大于等于 8 且数组长度大于等于 64 时,链表会转成红黑树。这是为了在哈希冲突严重时,将链表这种 O(n) 的查找时间复杂度优化为红黑树的 O(logn),提高查找效率。
- 线程状态:
- 新建(New):线程对象被创建,但还未调用 start 方法。
- 就绪(Runnable):线程调用了 start 方法,等待 CPU 调度执行。
- 运行(Running):线程获得 CPU 时间片正在执行。
- 阻塞(Blocked):线程因为某些原因放弃 CPU 使用权,暂停运行,如等待锁、I/O 操作等。
- 死亡(Terminated):线程执行完毕或因异常退出。
- 线程池核心参数及任务处理:
- 核心线程数(corePoolSize):线程池里常驻的线程数,即使这些线程处于空闲状态也不会被销毁。
- 最大线程数(maximumPoolSize):线程池能容纳的最大线程数。
- 阻塞队列(workQueue):用来存放任务的队列。当核心线程都在执行任务时,新任务会被放入阻塞队列。
- 任务处理流程:当有新任务提交时,先判断核心线程是否已满,若未满则创建核心线程处理任务;若核心线程已满,则将任务放入阻塞队列;若阻塞队列也满了,再判断是否能创建非核心线程(当前线程数小于最大线程数),若能则创建非核心线程处理任务;若线程数已达到最大线程数且阻塞队列已满,则触发拒绝策略。
- Spring Bean 的生命周期:
- 实例化(Instantiation):创建 Bean 的实例对象。
- 属性赋值(Populate):为 Bean 的属性设置值。
- 初始化(Initialization):调用 Bean 的初始化方法(如 @PostConstruct 注解的方法或实现 InitializingBean 接口的 afterPropertiesSet 方法)。
- 使用(Usage):Bean 可以被应用程序使用。
- 销毁(Destruction):当 Bean 不再使用时,调用销毁方法(如 @PreDestroy 注解的方法或实现 DisposableBean 接口的 destroy 方法)。
- Spring Boot 自动配置原理:Spring Boot 利用了 Spring 4.0 引入的条件化配置(@Conditional 注解)。通过大量的自动配置类(如 xxxAutoConfiguration),这些配置类根据项目中引入的依赖、配置文件等条件,决定是否要创建相应的 Bean 并注入到 Spring 容器中。例如,当项目引入 spring - data - jpa 依赖时,Spring Boot 会自动配置 JPA 相关的 Bean。同时,Spring Boot 还提供了 spring.factories 文件,在其中定义了自动配置类,Spring Boot 启动时会扫描这些文件并加载相应的自动配置类。
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行参数设置,这样能有效防止 SQL 注入。
- **{} 中的内容替换到 SQL 中,所以如果参数值来自用户输入,可能会导致 SQL 注入风险。一般用于传入数据库对象,如表名等。
- Dubbo 服务调用流程:
- 服务注册:服务提供者将自己提供的服务注册到注册中心(如 Zookeeper)。
- 服务订阅:消费者从注册中心订阅自己需要的服务,注册中心返回服务提供者的地址列表。
- 服务调用:消费者根据负载均衡算法从地址列表中选择一个服务提供者进行调用。调用过程中,Dubbo 会进行网络通信、序列化和反序列化等操作。
- RabbitMQ 常见应用场景:
- 异步处理:将一些耗时的操作(如发送邮件、生成报表等)封装成消息发送到 RabbitMQ 队列,主线程继续执行其他任务,提高系统响应速度。
- 解耦系统:不同系统之间通过 RabbitMQ 进行消息传递,降低系统之间的耦合度。例如,订单系统和库存系统通过消息队列进行交互,订单系统下单后发送消息,库存系统接收消息进行库存扣减,即使库存系统暂时不可用,订单系统也能正常下单,消息会在队列中等待库存系统恢复。
- 流量削峰:在高并发场景下,将大量请求以消息的形式发送到队列中,系统按照自己的处理能力从队列中消费消息,避免瞬间高并发请求对系统造成压力。
- xxl - job 任务调度原理:xxl - job 由调度中心、执行器和任务组成。调度中心负责任务的管理、调度触发等。执行器是任务的执行载体,负责接收调度中心的调度请求并执行任务。调度中心通过数据库存储任务信息和调度日志等。调度中心定时扫描数据库中需要执行的任务,根据任务的调度规则触发任务调度,向执行器发送调度请求,执行器接收到请求后执行具体任务,并将执行结果返回给调度中心。
- Redis 缓存雪崩、缓存穿透、缓存击穿及解决办法:
- 缓存雪崩:大量缓存同时过期,导致大量请求直接打到数据库,可能使数据库压力过大甚至崩溃。
- 解决办法:
- 给缓存设置不同的过期时间,避免大量缓存同时过期。
- 使用互斥锁,在缓存过期时,只有一个线程能获取锁去查询数据库并更新缓存,其他线程等待,防止大量请求同时查询数据库。
- 解决办法:
- 缓存穿透:查询一个不存在的数据,由于缓存中没有,每次都会查询数据库,若有大量这种请求,会对数据库造成压力。
- 解决办法:
- 布隆过滤器,在查询数据库前先通过布隆过滤器判断数据是否存在,若不存在则直接返回,避免查询数据库。
- 缓存空值,当查询数据库发现数据不存在时,也将空值缓存起来,并设置较短的过期时间,这样下次查询相同数据时直接从缓存返回空值,不再查询数据库。
- 解决办法:
- 缓存击穿:一个热点 key 过期瞬间,大量请求同时访问,这些请求会直接打到数据库。
- 解决办法:
- 互斥锁,和缓存雪崩中使用互斥锁类似,在热点 key 过期时,只有一个线程能获取锁去查询数据库并更新缓存,其他线程等待。
- 热点数据不过期,即不设置过期时间,通过其他机制(如定时任务)去更新缓存数据。
- 解决办法: