《互联网大厂 Java 面试:从基础到进阶的核心知识大考察》

111 阅读11分钟

第一轮面试 面试官:首先问几个基础问题。Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK8 之后还有红黑树。 面试官:不错。那 ArrayList 在扩容时是怎样的机制? 王铁牛:当元素个数达到容量的 0.75 倍时,就会进行扩容,新容量是原来的 1.5 倍。 面试官:回答得很准确。HashMap 在 JDK8 中,链表转红黑树的条件是什么? 王铁牛:当链表长度达到 8 且数组长度达到 64 时,链表会转成红黑树。 面试官:很好,基础掌握得不错。

第二轮面试 面试官:接下来问些多线程和 JUC 的问题。线程池有哪些核心参数? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位,还有任务队列。 面试官:嗯,那线程池的拒绝策略有哪些? 王铁牛:有 AbortPolicy,直接抛出异常;CallerRunsPolicy,让调用者线程来处理;DiscardOldestPolicy,丢弃队列最前面的任务;DiscardPolicy,直接丢弃任务。 面试官:还行。那说说多线程中 volatile 关键字的作用。 王铁牛:嗯……好像是保证可见性,还有防止指令重排。 面试官:回答得有点模糊,不过大致方向对。

第三轮面试 面试官:最后问些框架相关的。Spring 中 Bean 的作用域有哪些? 王铁牛:有 singleton 单例、prototype 原型、request、session 这些。 面试官:那 Spring Boot 自动配置的原理是什么? 王铁牛:呃……就是它会根据类路径下的依赖,自动配置一些 Bean 吧。 面试官:说的比较笼统。MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,{} 是字符串替换,#{} 能防止 SQL 注入。 面试官:好。Dubbo 服务暴露的过程是怎样的? 王铁牛:嗯……就是通过注册中心,把服务暴露出去,让消费者能调用。 面试官:回答得不太清晰。RabbitMQ 有哪些工作模式? 王铁牛:有简单模式、工作队列模式、发布订阅模式、路由模式、主题模式。 面试官:xxl - job 任务调度的核心原理是什么? 王铁牛:这个……就是它有调度中心,能管理任务调度。 面试官:Redis 缓存雪崩、缓存穿透、缓存击穿分别是什么,怎么解决? 王铁牛:缓存雪崩好像是大量缓存同时过期,缓存穿透是查询不存在的数据一直穿透到数据库,缓存击穿是一个 key 过期时大量请求过来。解决办法……我不太清楚。

面试总结:从这三轮面试来看,你在基础知识方面掌握得还算可以,像 ArrayList、HashMap 的底层结构,线程池的核心参数和拒绝策略等都回答得不错。但在一些进阶和框架原理方面,回答得不够清晰和深入,比如 Spring Boot 自动配置原理、Dubbo 服务暴露过程、xxl - job 任务调度核心原理以及 Redis 缓存相关问题的解决方案等。回去等通知吧,我们会综合评估所有面试者的情况,有消息会及时联系你。

答案:

  1. ArrayList 和 HashMap 的底层数据结构
    • ArrayList:底层是数组结构,它可以动态扩容,方便按索引快速访问元素。
    • HashMap:JDK7 及之前底层是数组加链表,JDK8 之后,当链表长度达到 8 且数组长度达到 64 时,链表会转成红黑树,以提高查找效率。数组用于存储哈希桶,链表或红黑树用于解决哈希冲突。
  2. ArrayList 扩容机制
    • ArrayList 有一个默认初始容量(通常为 10),当添加元素时,如果元素个数达到当前容量的 0.75 倍(即 loadFactor 为 0.75),就会触发扩容。
    • 扩容时,新容量是原来容量的 1.5 倍(通过位运算实现,newCapacity = oldCapacity + (oldCapacity >> 1)),然后将原数组的元素复制到新数组中。
  3. HashMap 链表转红黑树条件
    • 当链表长度达到 8 且数组长度达到 64 时,链表会转成红黑树。这是因为链表长度过长时,查找效率会降低,红黑树能保证在最坏情况下的查找时间复杂度为 O(logn),提高了查找性能。
  4. 线程池核心参数
    • 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut 为 true。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列满了且核心线程都在忙碌时,线程池会创建新线程,直到线程数达到最大线程数。
    • 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程存活的最长时间。超过这个时间,多余的线程会被销毁。
    • 时间单位(unit):存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 任务队列(workQueue):用于存放等待执行的任务。常见的任务队列有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)、SynchronousQueue(同步队列)等。
  5. 线程池拒绝策略
    • AbortPolicy:这是默认的拒绝策略,当任务无法提交到线程池(队列已满且线程数达到最大线程数)时,直接抛出 RejectedExecutionException 异常。
    • CallerRunsPolicy:当任务被拒绝时,会将任务交给调用者线程来执行,这样可以降低新任务的提交速度,减轻线程池的压力。
    • DiscardOldestPolicy:丢弃队列中最老的任务(即队列头部的任务),然后尝试提交新任务。
    • DiscardPolicy:直接丢弃被拒绝的任务,不做任何处理。
  6. volatile 关键字作用
    • 保证可见性:当一个变量被 volatile 修饰时,对这个变量的写操作会立即刷新到主内存,而读操作会直接从主内存读取,保证了不同线程之间对该变量的操作的可见性。例如,线程 A 修改了 volatile 修饰的变量,线程 B 能马上看到修改后的结果。
    • 防止指令重排:在编译和运行时,为了提高性能,编译器和处理器可能会对指令进行重排序。volatile 关键字可以禁止特定类型的指令重排,确保 volatile 变量的操作顺序与代码顺序一致。
  7. Spring Bean 的作用域
    • singleton:单例模式,在整个 Spring 容器中,一个 Bean 定义只会创建一个实例。这是默认的作用域。
    • prototype:原型模式,每次请求获取 Bean 时,都会创建一个新的实例。
    • request:在一次 HTTP 请求中,一个 Bean 定义只会创建一个实例。适用于 Web 应用中与请求相关的 Bean。
    • session:在一个 HTTP Session 中,一个 Bean 定义只会创建一个实例。同样适用于 Web 应用中与 Session 相关的 Bean。
  8. Spring Boot 自动配置原理
    • Spring Boot 利用了 Spring 4.0 引入的条件化配置(@Conditional)功能。
    • 在启动过程中,Spring Boot 会扫描 classpath 下的 META - INF/spring.factories 文件,该文件中定义了各种自动配置类。
    • 自动配置类通过 @Conditional 注解及其相关的派生注解(如 @ConditionalOnClass、@ConditionalOnProperty 等)来判断是否满足自动配置的条件。例如,@ConditionalOnClass 表示当类路径下存在某个类时才进行配置,@ConditionalOnProperty 表示当配置文件中存在某个属性时才进行配置。如果满足条件,就会将相关的 Bean 定义注册到 Spring 容器中。
  9. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 的 set 方法来设置参数值,这样可以有效防止 SQL 注入。例如,select * from user where username = #{username},实际执行时会变成 select * from user where username =?,然后通过 PreparedStatement 设置具体的 username 值。
    • **:是字符串替换,MyBatis在处理{}**:是字符串替换,MyBatis 在处理 {} 时,会直接将 替换为变量的值。例如,selectfromuserwhereusername={} 替换为变量的值。例如,`select * from user where username = '{username}',如果 username 为 'test',实际执行的 SQL 就是 select * from user where username = 'test'`。由于是直接替换,所以存在 SQL 注入风险,一般用于传入数据库对象,如表名等。
  10. Dubbo 服务暴露过程
  • 配置解析:Dubbo 会读取配置文件(如 XML 配置或注解配置),解析出服务接口、实现类、注册中心地址等信息。
  • 代理创建:通过动态代理技术(如 Javassist 或 JDK 动态代理)为服务实现类创建代理对象,这个代理对象用于处理远程调用相关逻辑。
  • 协议绑定:根据配置选择合适的协议(如 Dubbo 协议、HTTP 协议等),将服务绑定到指定的端口。
  • 注册中心注册:将服务的元数据(包括服务接口、版本、地址等信息)注册到注册中心(如 Zookeeper、Redis 等),以便消费者能够发现服务。
  1. RabbitMQ 工作模式
  • 简单模式:一个生产者,一个消费者,一条队列。生产者将消息发送到队列,消费者从队列中获取消息。
  • 工作队列模式:一个生产者,多个消费者,一条队列。多个消费者竞争从队列中获取消息,每个消息只会被一个消费者处理。
  • 发布订阅模式:一个生产者,多个消费者,一个交换机(Exchange)和多个队列。生产者将消息发送到交换机,交换机将消息广播到所有绑定的队列,每个队列的消费者都能收到消息。
  • 路由模式:一个生产者,多个消费者,一个交换机和多个队列。交换机根据路由键(routing key)将消息发送到指定的队列,队列在绑定交换机时需要指定路由键。
  • 主题模式:与路由模式类似,但路由键支持通配符。交换机根据通配符匹配将消息发送到相应的队列。
  1. xxl - job 任务调度核心原理
  • 调度中心:是 xxl - job 的核心组件,负责管理任务、调度任务、监控任务执行状态等。它基于数据库存储任务信息,通过定时任务(如 Quartz)触发任务调度。
  • 执行器:部署在业务系统中,负责接收调度中心的调度请求并执行任务。执行器与调度中心通过 HTTP 进行通信,将任务执行结果返回给调度中心。
  • 任务注册:执行器启动时,会向调度中心注册自己,并上报可执行的任务列表。调度中心将这些任务信息存储在数据库中。
  • 任务调度:调度中心根据任务的调度策略(如 cron 表达式),定时触发任务调度,向对应的执行器发送调度请求。执行器接收到请求后,执行具体的任务逻辑,并将执行结果返回给调度中心。
  1. Redis 缓存雪崩、缓存穿透、缓存击穿及解决方案
  • 缓存雪崩:大量缓存同时过期,导致大量请求直接访问数据库,可能使数据库压力过大甚至崩溃。
    • 解决方案
      • 设置不同过期时间:给缓存设置随机的过期时间,避免大量缓存同时过期。
      • 使用互斥锁:在缓存过期时,使用互斥锁(如 Redis 的 SETNX 命令)保证只有一个线程去查询数据库并更新缓存,其他线程等待,从而防止大量请求同时访问数据库。
  • 缓存穿透:查询一个不存在的数据,由于缓存中没有,每次都会查询数据库,若有大量这种请求,会对数据库造成压力。
    • 解决方案
      • 布隆过滤器:在查询前先通过布隆过滤器判断数据是否存在,若不存在则直接返回,不会查询数据库。布隆过滤器通过多个哈希函数对数据进行映射,将结果存储在一个位数组中,查询时通过同样的哈希函数映射判断数据是否存在,但可能存在误判。
      • 缓存空值:当查询数据库发现数据不存在时,也将空值缓存起来,并设置较短的过期时间,这样下次查询相同数据时,直接从缓存中获取空值,不会查询数据库。
  • 缓存击穿:一个热点 key 过期时,大量请求同时过来访问该 key,由于缓存过期,这些请求会直接访问数据库。
    • 解决方案
      • 使用互斥锁:与缓存雪崩中使用互斥锁类似,在热点 key 过期时,使用互斥锁保证只有一个线程去查询数据库并更新缓存,其他线程等待。
      • 设置热点 key 永不过期:对于一些热点数据,可以设置为永不过期,同时通过其他机制(如定时任务)来更新数据。