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

47 阅读10分钟

第一轮面试 面试官:先问些基础的。Java 中多线程创建方式有哪些? 王铁牛:可以通过继承 Thread 类,还有实现 Runnable 接口来创建多线程。 面试官:嗯,回答得不错。那线程池有哪些核心参数? 王铁牛:有核心线程数、最大线程数,还有……还有那个阻塞队列。 面试官:回答得还行。ArrayList 和 LinkedList 在数据存储和性能上有什么区别? 王铁牛:ArrayList 是基于数组的,查询快,增删慢;LinkedList 是链表结构,增删快,查询慢。 面试官:不错,基础掌握得还行。

第二轮面试 面试官:现在深入点。HashMap 在 JDK1.7 和 JDK1.8 中有什么主要区别? 王铁牛:嗯……1.8 好像是用红黑树了,1.7 是链表,其他的……不太清楚了。 面试官:那 Spring 框架中 IOC 和 AOP 分别是什么,有什么作用? 王铁牛:IOC 是控制反转,就是把对象创建交给 Spring 容器。AOP 嘛,好像是面向切面编程,能实现一些功能增强。具体咋实现的,我有点模糊。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 好像是直接拼接 SQL 语句,不安全。但具体使用场景,我不太确定。

第三轮面试 面试官:再问几个更难的。Dubbo 的服务调用流程是怎样的? 王铁牛:嗯……就是服务提供者注册服务,消费者去调用,中间好像还有注册中心啥的,具体细节我不太记得了。 面试官:RabbitMQ 如何保证消息的可靠性? 王铁牛:嗯……好像有确认机制,还有持久化啥的,具体怎么操作,我一下子想不起来了。 面试官:xxl - job 调度任务的原理是什么? 王铁牛:这个……我就知道它是个调度框架,原理不太清楚。

面试官:今天的面试就到这里。你整体基础知识还行,但对于一些进阶和深入的知识点掌握得不够扎实。回去等通知吧,我们会综合评估所有候选人后,再做决定。

问题答案

  1. Java 中多线程创建方式
    • 继承 Thread 类:创建一个类继承 Thread 类,重写 run 方法,在 run 方法中编写线程执行的逻辑。通过创建该类的实例,调用 start 方法启动线程。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行中");
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
- **实现 Runnable 接口**:创建一个类实现 Runnable 接口,实现 run 方法。将该类的实例作为参数传递给 Thread 类的构造函数,然后调用 Thread 的 start 方法启动线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行中");
    }
}
public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}
  1. 线程池核心参数
    • 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut 为 true。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列满了且核心线程都在忙碌时,线程池会创建新的线程,直到达到最大线程数。
    • 阻塞队列(workQueue):用于存放等待执行的任务。常见的阻塞队列有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。例如 ArrayBlockingQueue 是基于数组的有界阻塞队列,LinkedBlockingQueue 是基于链表的无界阻塞队列(也可以指定容量变成有界),SynchronousQueue 不存储元素,每个插入操作必须等待另一个线程的移除操作。
    • 线程存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程的存活时间。即这些空闲线程在多长时间内没有任务执行就会被销毁。
    • 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
  2. ArrayList 和 LinkedList 在数据存储和性能上的区别
    • 数据存储
      • ArrayList:基于动态数组实现,它在内存中是连续存储的。这意味着可以通过索引快速访问元素,因为内存地址是连续的,计算元素地址很方便。例如访问 ArrayList 中第 n 个元素,直接通过数组的偏移量就可以获取到。
      • LinkedList:基于双向链表实现,每个节点包含前驱节点、后继节点和数据。节点在内存中不是连续存储的,通过指针来连接各个节点。
    • 性能
      • 查询:ArrayList 查询速度快,因为可以通过索引直接定位元素。而 LinkedList 查询需要从头或尾开始遍历链表,直到找到目标元素,时间复杂度为 O(n),所以查询性能相对较差。
      • 增删:在 ArrayList 中间插入或删除元素时,需要移动后续元素,时间复杂度为 O(n);在链表头部或尾部增删元素,LinkedList 只需修改指针,时间复杂度为 O(1),所以在增删操作尤其是频繁在头部或尾部操作时,LinkedList 性能更好。但如果在 ArrayList 尾部添加元素,由于动态数组的特性,平均时间复杂度接近 O(1)。
  3. HashMap 在 JDK1.7 和 JDK1.8 中的主要区别
    • 数据结构
      • JDK1.7:采用数组 + 链表的数据结构。当发生哈希冲突时,新的元素会以头插法插入到链表头部。
      • JDK1.8:采用数组 + 链表 + 红黑树的数据结构。当链表长度大于 8 且数组长度大于等于 64 时,链表会转换为红黑树,以提高查询效率。当红黑树节点数小于 6 时,又会退化为链表。新元素采用尾插法插入链表。
    • 哈希算法
      • JDK1.7:哈希算法相对复杂,需要进行 4 次位运算和 5 次异或运算。
      • JDK1.8:简化了哈希算法,只进行了 1 次位运算和 1 次异或运算,提高了计算效率。
    • 扩容机制
      • JDK1.7:扩容时,需要重新计算每个元素的哈希值并重新插入到新的数组中,较为耗时。
      • JDK1.8:在扩容时,对于链表节点,会根据节点哈希值与旧容量的关系,将节点分为高位和低位,分别插入到新数组的不同位置,减少了重新计算哈希值的开销。
  4. Spring 框架中 IOC 和 AOP 分别是什么,有什么作用
    • IOC(控制反转)
      • 概念:将对象的创建和管理控制权从应用程序代码转移到 Spring 容器。在传统编程中,对象的创建和依赖关系管理由开发者在代码中手动完成,而 IOC 容器负责创建对象、管理对象的生命周期以及对象之间的依赖关系。
      • 作用:解耦组件之间的依赖关系,提高代码的可维护性和可测试性。例如,一个 Service 类依赖另一个 Dao 类,如果在 Service 类中手动创建 Dao 实例,那么 Service 类与 Dao 类的耦合度就很高。使用 IOC 后,Spring 容器会创建并注入 Dao 实例到 Service 中,Service 类只需要使用 Dao 提供的方法,无需关心 Dao 的创建细节。
    • AOP(面向切面编程)
      • 概念:将一些与业务核心逻辑无关的功能(如日志记录、事务管理、权限控制等)提取出来,形成一个独立的模块(切面),然后通过动态代理等技术将这些切面功能织入到业务逻辑中。
      • 作用:提高代码的复用性和可维护性,避免在业务代码中大量重复编写非核心功能代码。例如,在多个 Service 方法中都需要记录日志,如果在每个方法中都编写日志记录代码,会导致代码冗余。使用 AOP 可以将日志记录功能封装成一个切面,统一织入到需要记录日志的方法中。
  5. MyBatis 中 #{} 和 ${} 的区别
    • #{}
      • 原理:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,并将参数值作为 PreparedStatement 的参数进行设置。
      • 优点:可以有效防止 SQL 注入攻击,因为参数值不会直接拼接到 SQL 语句中。例如,当参数值为恶意 SQL 语句时,#{} 会将其作为普通字符串处理,不会被数据库解析为 SQL 指令。
      • 适用场景:适用于大多数参数传递场景,尤其是涉及用户输入的参数。
    • ${}
      • 原理:是字符串替换,MyBatis 在处理 时,会直接将{} 时,会直接将 {} 替换为参数值,参数值会直接拼接到 SQL 语句中。
      • 缺点:存在 SQL 注入风险,如果参数值是恶意 SQL 语句,会被数据库解析并执行,导致数据泄露或其他安全问题。
      • 适用场景:一般用于传入数据库对象,如表名、列名等,因为这些情况无法使用预编译。但使用时要确保参数值的安全性,通常由开发者在代码中进行严格的校验。
  6. Dubbo 的服务调用流程
    • 服务注册:服务提供者启动时,将自己提供的服务注册到注册中心(如 Zookeeper)。注册中心保存了服务提供者的地址、端口、服务接口等信息。
    • 服务订阅:服务消费者启动时,向注册中心订阅自己需要的服务。注册中心会将服务提供者的信息返回给服务消费者。
    • 服务调用:服务消费者根据从注册中心获取的服务提供者信息,通过网络通信(如 Netty)调用服务提供者的接口。在调用过程中,Dubbo 会进行负载均衡,从多个服务提供者中选择一个来处理请求。常见的负载均衡策略有随机、轮询、最少活跃调用数等。
    • 服务监控:Dubbo 提供了监控中心,用于统计服务的调用次数、调用时间等信息。服务提供者和消费者在调用前后会向监控中心发送统计信息,方便开发者了解服务的运行状况。
  7. RabbitMQ 如何保证消息的可靠性
    • 消息持久化
      • 队列持久化:通过将队列声明为持久化队列,当 RabbitMQ 服务器重启后,队列依然存在。在声明队列时,将 durable 参数设置为 true 即可。
      • 消息持久化:将消息的 deliveryMode 属性设置为 2,表示消息为持久化消息。这样当消息到达队列后,会被写入磁盘,即使服务器重启,消息也不会丢失。
    • 确认机制
      • 生产者确认(publisher confirm):生产者发送消息后,RabbitMQ 会给生产者返回一个确认消息,告知生产者消息是否成功到达服务器。生产者可以通过设置 confirm 模式,监听确认回调函数来处理确认结果。如果消息未成功确认,生产者可以进行重试等操作。
      • 消费者确认(consumer ack):消费者接收并处理完消息后,需要向 RabbitMQ 发送一个确认消息(ack),告知 RabbitMQ 该消息已被成功处理。如果 RabbitMQ 未收到消费者的确认消息,会认为消息处理失败,可能会将消息重新发送给其他消费者或重新放入队列等待再次处理。
    • 备份交换机(alternate exchange):当消息发送到交换机,但没有匹配的队列时,可以将这些消息发送到备份交换机,备份交换机可以将消息路由到一个特殊的队列进行存储或处理,避免消息丢失。
  8. xxl - job 调度任务的原理
    • 调度中心:是 xxl - job 的核心组件,负责管理调度任务、触发任务执行、监控任务执行状态等。调度中心基于 Quartz 框架实现任务调度,通过数据库存储任务信息(如任务名称、执行时间表达式、任务参数等)。
    • 执行器:部署在业务系统中,负责接收调度中心发送的任务并执行。执行器启动时会向调度中心注册自己,调度中心会维护执行器的在线状态。
    • 任务触发:调度中心根据任务的执行时间表达式(如 Cron 表达式),在合适的时间触发任务。调度中心通过网络通信(如 HTTP)向注册的执行器发送任务执行请求。
    • 任务执行:执行器接收到任务请求后,根据任务类型(如 Java 方法调用、Shell 脚本执行等)执行相应的任务逻辑。执行完成后,将任务执行结果返回给调度中心。
    • 日志记录:执行器在任务执行过程中,会记录详细的执行日志,并将日志信息上报给调度中心。调度中心可以展示任务的执行日志,方便开发者排查问题。