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

36 阅读13分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 LinkedList 的区别是什么? 王铁牛:ArrayList 是基于数组实现的,随机访问快,增删慢;LinkedList 基于链表,增删快,随机访问慢。 面试官:不错,回答得很清晰。那 HashMap 在 JDK1.7 和 JDK1.8 中有什么主要区别? 王铁牛:1.8 用红黑树代替了 1.7 的链表,当链表长度超过阈值会转成红黑树,提高查找效率。 面试官:很好。Spring 框架中 IOC 和 AOP 分别是什么? 王铁牛:IOC 是控制反转,把对象创建和管理交给 Spring 容器;AOP 是面向切面编程,在不修改代码的情况下增加功能。 面试官:回答得很好,基础很扎实。

第二轮面试 面试官:聊聊多线程,线程池的核心参数有哪些,分别有什么作用? 王铁牛:嗯……有核心线程数,最大线程数,还有……还有队列容量,核心线程数就是一开始创建的线程数,最大线程数就是最多能创建的线程数。 面试官:那队列容量具体有什么作用呢? 王铁牛:就是放任务的地方,任务多了就放进去。 面试官:好,那 JVM 的内存模型了解吗,简单说说。 王铁牛:JVM 内存模型……有堆、栈,堆是放对象的,栈是放局部变量的。 面试官:好,继续,MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 是直接替换,可能有 SQL 注入风险。

第三轮面试 面试官:Dubbo 框架的服务调用流程是怎样的? 王铁牛:嗯……就是服务提供者注册服务,消费者去调用,中间好像还有个注册中心。 面试官:那注册中心具体起什么作用呢? 王铁牛:就是存放服务信息的,让消费者能找到服务提供者。 面试官:RabbitMQ 中的消息确认机制是怎么回事? 王铁牛:就是消息发出去,要确认对方收到了,好像有个什么 confirm 模式。 面试官:xxl - job 是什么,简单介绍下。 王铁牛:它是个分布式任务调度平台,能管理任务调度。 面试官:Redis 缓存雪崩、缓存穿透、缓存击穿分别是什么,怎么解决? 王铁牛:缓存雪崩好像是大量缓存同时过期,缓存穿透是查询不存在的数据,缓存击穿是热点数据过期……解决办法……我想想,缓存雪崩可以设置不同过期时间,缓存穿透用布隆过滤器,缓存击穿加互斥锁。

面试总结:从这三轮面试来看,你在基础知识方面掌握得还不错,像 ArrayList 和 LinkedList 的区别、HashMap 的版本区别、Spring 的 IOC 和 AOP 等都回答得很好。但在一些进阶和深入的问题上,比如线程池核心参数的具体作用、JVM 内存模型细节、Dubbo 服务调用流程及注册中心作用等,回答得不是特别清晰和全面。对于 RabbitMQ 的消息确认机制、xxl - job 的介绍以及 Redis 缓存问题的解决办法,虽然有一定了解,但表述上还可以更完善。整体来说,我们会综合考量所有面试者的情况,你回去等通知吧,无论结果如何,我们都会在一周内给你回复。

问题答案

  1. ArrayList 和 LinkedList 的区别
    • 数据结构:ArrayList 基于动态数组实现,内存地址连续;LinkedList 基于双向链表实现,节点在内存中不连续。
    • 随机访问:ArrayList 支持随机访问,通过索引直接定位元素,时间复杂度为 O(1);LinkedList 随机访问效率低,需要从头或尾遍历,时间复杂度为 O(n)。
    • 增删操作:ArrayList 在数组中间或开头增删元素时,需要移动大量元素,时间复杂度为 O(n);在尾部增删效率较高,时间复杂度为 O(1)。LinkedList 在任意位置增删元素只需修改前后节点的引用,时间复杂度为 O(1)。
  2. HashMap 在 JDK1.7 和 JDK1.8 中的区别
    • 数据结构:JDK1.7 采用数组 + 链表结构;JDK1.8 采用数组 + 链表 + 红黑树结构。当链表长度超过阈值(默认为 8)且数组容量大于等于 64 时,链表会转化为红黑树,以提高查找效率。
    • 哈希冲突解决方式:JDK1.7 主要采用头插法解决哈希冲突;JDK1.8 采用尾插法,避免了 JDK1.7 中在多线程环境下头插法可能导致的链表成环问题。
    • 扩容机制:JDK1.7 扩容时,需要重新计算每个元素的哈希值并重新插入到新的数组中;JDK1.8 优化了扩容机制,部分元素在扩容时不需要重新计算哈希值,直接根据原索引和新容量的关系进行迁移。
  3. Spring 框架中 IOC 和 AOP
    • IOC(控制反转):将对象的创建和管理从应用程序代码转移到 Spring 容器中。通过依赖注入(DI),Spring 容器负责创建对象,并将对象之间的依赖关系注入到相应的组件中。这样可以降低组件之间的耦合度,提高代码的可维护性和可测试性。例如,一个 Service 类依赖于一个 Dao 类,在传统方式下,Service 类需要自己创建 Dao 实例;而在 Spring 中,Spring 容器会创建 Dao 实例并注入到 Service 类中。
    • AOP(面向切面编程):将一些与业务逻辑无关但又贯穿于多个业务模块的功能(如日志记录、事务管理、权限控制等)抽取出来,形成一个独立的切面。通过动态代理或字节码增强等技术,在不修改原有业务代码的情况下,将切面功能织入到目标方法的执行前后。例如,在方法执行前记录日志,在方法执行后进行事务提交等操作。
  4. 线程池的核心参数
    • 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁。当有新任务提交时,如果线程池中的线程数小于 corePoolSize,会创建新的线程来处理任务。
    • 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数。当任务队列已满,且线程池中的线程数小于 maximumPoolSize 时,会创建新的线程来处理任务。
    • 队列容量(workQueue):用于存放等待执行的任务。当线程池中的线程数达到 corePoolSize 时,新提交的任务会被放入队列中等待执行。常见的队列类型有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。
    • 线程存活时间(keepAliveTime):当线程池中的线程数大于 corePoolSize 时,多余的空闲线程在存活时间内没有新任务可执行,就会被销毁。
    • 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
  5. JVM 的内存模型
    • 堆(Heap):是 JVM 中最大的一块内存区域,用于存储对象实例和数组。堆被所有线程共享,是垃圾回收的主要区域。根据对象的生命周期不同,堆又可以分为新生代和老年代。新生代主要存放新创建的对象,老年代存放经过多次垃圾回收后仍然存活的对象。
    • 栈(Stack):每个线程都有自己独立的栈,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。栈的生命周期与线程相同,随着线程的创建而创建,随着线程的结束而销毁。栈中的数据以栈帧的形式存在,每个方法调用都会创建一个新的栈帧。
    • 方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也是被所有线程共享的。在 JDK8 及以后,方法区被元空间(Meta Space)取代,元空间使用本地内存。
    • 程序计数器(Program Counter Register):每个线程都有一个程序计数器,它记录着当前线程所执行的字节码的行号。在多线程环境下,程序计数器用于保证线程切换后能恢复到正确的执行位置。
    • 本地方法栈(Native Method Stack):与 Java 栈类似,只不过它是为 JVM 执行本地方法(用 C、C++ 等语言编写的方法)服务的。
  6. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行设置参数值。这种方式可以有效防止 SQL 注入攻击,因为参数值会被当作字符串处理,不会与 SQL 语句混淆。例如,SQL 语句为 SELECT * FROM user WHERE username = #{username},实际执行时会变为 SELECT * FROM user WHERE username =?,然后通过 PreparedStatement 设置参数值。
    • **:是字符串替换,MyBatis在处理{}**:是字符串替换,MyBatis 在处理 {} 时,会直接将 中的内容替换到SQL语句中。如果参数值来自用户输入,可能会导致SQL注入风险。例如,SQL语句为SELECTFROMuserWHEREusername={} 中的内容替换到 SQL 语句中。如果参数值来自用户输入,可能会导致 SQL 注入风险。例如,SQL 语句为 `SELECT * FROM user WHERE username = '{username}',如果用户输入 ' OR '1'='1,则实际执行的 SQL 语句会变为 SELECT * FROM user WHERE username = '' OR '1'='1'`,这会导致查询出所有用户数据。
  7. Dubbo 框架的服务调用流程
    • 服务注册:服务提供者启动时,将自己提供的服务注册到注册中心(如 Zookeeper)。注册中心保存了服务提供者的地址、端口、服务接口等信息。
    • 服务订阅:服务消费者启动时,向注册中心订阅自己需要的服务。注册中心会将服务提供者的信息推送给服务消费者。
    • 服务调用:服务消费者根据从注册中心获取的服务提供者信息,通过网络调用服务提供者的接口。Dubbo 支持多种通信协议,如 Dubbo 协议、HTTP 协议等。在调用过程中,Dubbo 会进行负载均衡,从多个服务提供者中选择一个来处理请求。常见的负载均衡策略有随机、轮询、最少活跃调用数等。
    • 服务监控:Dubbo 提供了监控中心,用于统计服务的调用次数、调用时间等信息。服务提供者和消费者在调用前后会向监控中心发送统计信息,方便开发人员进行性能分析和问题排查。
  8. RabbitMQ 中的消息确认机制
    • 生产者确认(Publisher Confirm)
      • confirm 模式:生产者将信道设置为 confirm 模式后,所有在该信道上发布的消息都会被分配一个唯一的 ID。当消息被成功投递到目标队列后,RabbitMQ 会发送一个确认消息给生产者,包含该消息的 ID。如果消息投递失败,RabbitMQ 也会发送一个 nack(negative - acknowledgment)消息给生产者,告知消息投递失败。生产者可以通过监听这些确认和 nack 消息来处理消息的发送结果。
      • return 模式:当消息无法路由到任何一个匹配的队列时,RabbitMQ 会将消息返回给生产者。生产者需要在发送消息时设置 mandatory 标志为 true,并监听 ReturnListener 来接收返回的消息。
    • 消费者确认(Consumer Acknowledgment)
      • 自动确认(Auto - Ack):消费者从队列中获取消息后,RabbitMQ 会自动将该消息标记为已确认,即使消费者还未真正处理完消息。这种方式可能会导致消息丢失,因为如果消费者在处理消息过程中崩溃,该消息已经被确认,RabbitMQ 不会再重新投递。
      • 手动确认(Manual - Ack):消费者从队列中获取消息后,需要手动调用确认方法(如 basicAck)来告知 RabbitMQ 消息已被成功处理。如果消费者在处理消息过程中崩溃,没有调用确认方法,RabbitMQ 会认为消息未被处理,会将该消息重新投递给其他消费者或重新放入队列等待下一次投递。
  9. xxl - job 是什么
    • xxl - job 是一个轻量级分布式任务调度平台,具有简单易用、功能丰富等特点。它主要由调度中心、执行器和任务组成。
    • 调度中心:负责管理调度任务,包括任务的新增、修改、删除、暂停、恢复等操作。同时,调度中心会按照设定的调度规则触发任务执行,并监控任务的执行状态。调度中心支持集群部署,以提高系统的可用性和稳定性。
    • 执行器:负责实际执行任务,它是任务的运行载体。执行器可以部署在不同的服务器上,与调度中心进行通信。执行器接收到调度中心的任务触发请求后,会启动相应的任务线程来执行任务,并将任务执行结果返回给调度中心。
    • 任务:是具体要执行的业务逻辑,可以是一段 Java 代码、Shell 脚本、Python 脚本等。任务通过在调度中心进行配置,与执行器进行关联,实现分布式任务调度。
  10. Redis 缓存雪崩、缓存穿透、缓存击穿及解决办法
  • 缓存雪崩
    • 问题描述:大量缓存同时过期,导致大量请求直接访问数据库,可能使数据库压力过大甚至崩溃。
    • 解决办法
      • 设置不同过期时间:在设置缓存过期时间时,为每个缓存设置一个随机的过期时间,避免大量缓存同时过期。例如,原本设置缓存过期时间为 1 小时,可以改为在 50 分钟到 70 分钟之间随机设置过期时间。
      • 使用互斥锁:在缓存过期时,只允许一个线程去查询数据库并更新缓存,其他线程等待。这样可以防止大量线程同时查询数据库。
      • 后台更新缓存:在缓存过期前,提前使用异步任务更新缓存,保证缓存不会出现大量同时过期的情况。
  • 缓存穿透
    • 问题描述:查询一个不存在的数据,由于缓存中也没有,每次都会查询数据库,若有大量这样的请求,会导致数据库压力增大。
    • 解决办法
      • 布隆过滤器:在查询数据库之前,先通过布隆过滤器判断数据是否存在。布隆过滤器是一种概率型数据结构,它可以快速判断一个元素是否在集合中。如果布隆过滤器判断数据不存在,则直接返回,不会查询数据库。虽然布隆过滤器可能存在误判,但可以通过调整参数来降低误判率。
      • 缓存空值:当查询数据库发现数据不存在时,将空值也缓存起来,并设置一个较短的过期时间,这样下次查询同样的数据时,直接从缓存中获取空值,避免查询数据库。
  • 缓存击穿
    • 问题描述:热点数据的缓存过期瞬间,大量请求同时访问,直接查询数据库,可能导致数据库压力过大。
    • 解决办法
      • 加互斥锁:在缓存过期时,使用互斥锁(如 Redis 的 SETNX 命令)保证只有一个线程去查询数据库并更新缓存,其他线程等待。当更新完缓存后,释放互斥锁,其他线程可以从缓存中获取数据。
      • 设置热点数据永不过期:对于热点数据,不设置过期时间,同时通过其他机制(如定时任务)来更新缓存数据,保证数据的一致性。