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

34 阅读10分钟

第一轮面试 面试官:首先问几个基础问题。Java 中 ArrayList 和 LinkedList 的区别是什么? 王铁牛:ArrayList 是基于数组实现的,随机访问快,增删慢,尤其是在中间位置增删。LinkedList 是基于链表实现的,增删快,随机访问慢。 面试官:回答得不错。那 HashMap 在 JDK1.7 和 JDK1.8 中有什么主要区别? 王铁牛:1.8 中引入了红黑树,当链表长度超过阈值(8)时会转化为红黑树,提高查找效率。1.7 只有链表。 面试官:很好。Spring 框架中 IOC 和 AOP 分别是什么? 王铁牛:IOC 是控制反转,把对象创建和管理的控制权交给 Spring 容器。AOP 是面向切面编程,在不修改原有代码的基础上增加功能,比如日志、事务。

第二轮面试 面试官:接下来深入一些。多线程中线程池的核心参数有哪些,分别有什么作用? 王铁牛:嗯……有核心线程数,就是一直存活的线程数。还有最大线程数,就是能创建的最大线程数量。还有队列,用来存放任务。 面试官:还行。那 JVM 的内存模型了解吗,简单说说。 王铁牛:JVM 内存模型分堆、栈、方法区这些,堆是存放对象实例的,栈是线程私有的,存放局部变量这些。 面试官:好。MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,能防止 SQL 注入,{} 是字符串替换,可能有 SQL 注入风险。

第三轮面试 面试官:最后问几个更难的。Dubbo 的服务调用流程是怎样的? 王铁牛:嗯……好像是服务提供者注册到注册中心,消费者从注册中心获取服务,然后就调用了。 面试官:说的不太清晰。RabbitMQ 如何保证消息的可靠性? 王铁牛:嗯……好像有确认机制,还有持久化什么的。 面试官:回答得比较模糊。xxl - job 任务调度框架的核心原理是什么? 王铁牛:这个……就是能调度任务,具体原理不太清楚。

面试官总结:王铁牛,今天的面试就到这里。整体来看,你对一些基础知识掌握得还可以,但对于一些进阶和深入的知识点,理解还不够透彻。我们后续会综合评估所有候选人,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。希望你回去后能继续深入学习相关知识,提升自己的技术水平。

问题答案

  1. ArrayList 和 LinkedList 的区别
    • 数据结构:ArrayList 基于动态数组,内存连续;LinkedList 基于双向链表,内存不连续。
    • 随机访问:ArrayList 支持随机访问,通过索引直接定位元素,时间复杂度为 O(1);LinkedList 随机访问慢,需从头或尾遍历,时间复杂度为 O(n)。
    • 增删操作:ArrayList 在中间或开头增删元素时,需移动大量元素,时间复杂度为 O(n);LinkedList 增删只需修改指针,时间复杂度为 O(1),但如果要先定位元素,时间复杂度还是 O(n)。
  2. HashMap 在 JDK1.7 和 JDK1.8 的区别
    • 数据结构:JDK1.7 采用数组 + 链表;JDK1.8 采用数组 + 链表 + 红黑树。当链表长度大于 8 且数组容量大于 64 时,链表会转化为红黑树,以提高查找效率。
    • 哈希冲突解决方式:JDK1.7 采用头插法,在多线程环境下可能形成环形链表导致死循环;JDK1.8 采用尾插法,避免了这个问题。
    • 扩容机制:JDK1.7 扩容时,需重新计算哈希值和索引位置;JDK1.8 优化了扩容算法,部分元素的索引位置不变,减少了重新计算的开销。
  3. Spring 框架中 IOC 和 AOP
    • IOC(控制反转):将对象的创建和管理从应用程序代码转移到 Spring 容器中。通过依赖注入(DI),容器将对象依赖的其他对象注入到目标对象中。例如,一个 Service 类依赖一个 Dao 类,在传统方式中需要在 Service 类中手动创建 Dao 实例,而在 Spring 中,Spring 容器会创建 Dao 实例并注入到 Service 中。这样降低了组件之间的耦合度,提高了代码的可维护性和可测试性。
    • AOP(面向切面编程):将一些通用功能(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,形成独立的切面。这些切面可以在不修改原有业务代码的情况下,在特定的连接点(如方法调用前后)织入到业务逻辑中。例如,在一个业务方法执行前记录日志,在方法执行后提交事务,通过 AOP 可以优雅地实现这些功能,而不需要在每个业务方法中重复编写相关代码。
  4. 线程池的核心参数
    • 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut 为 true。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数量。当任务队列已满且活动线程数小于最大线程数时,线程池会创建新的线程来处理任务。
    • 队列(workQueue):用于存放等待执行的任务。常见的队列类型有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)、SynchronousQueue(同步队列)等。不同的队列类型会影响线程池的性能和行为。
    • 线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,多余的空闲线程在存活时间后会被销毁。
    • 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 拒绝策略(RejectedExecutionHandler):当任务队列已满且线程数达到最大线程数时,新提交的任务会被拒绝,此时会执行拒绝策略。常见的拒绝策略有 AbortPolicy(抛出异常)、CallerRunsPolicy(由调用者线程处理任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交新任务)。
  5. JVM 内存模型
    • 堆(Heap):是 JVM 中最大的一块内存区域,被所有线程共享,用于存放对象实例和数组。堆可以进一步分为新生代(Young Generation)和老年代(Old Generation),新生代又分为 Eden 区和两个 Survivor 区(S0 和 S1)。对象通常在 Eden 区创建,经过多次垃圾回收后,如果对象依然存活,会被移动到老年代。
    • 栈(Stack):每个线程都有自己的栈,用于存放局部变量表、操作数栈、动态链接、方法出口等信息。栈的生命周期与线程相同,线程结束,栈也随之销毁。栈中的数据以栈帧的形式存在,每个方法调用都会创建一个栈帧。
    • 方法区(Method Area):被所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK8 及以后,方法区被元空间(Metaspace)取代,元空间使用本地内存,不再受堆内存大小限制。
    • 程序计数器(Program Counter Register):每个线程都有一个程序计数器,是线程私有的,用于记录当前线程执行的字节码指令地址。如果执行的是本地方法,则程序计数器的值为空。
    • 本地方法栈(Native Method Stack):与 Java 虚拟机栈类似,只不过它是为虚拟机使用到的本地(Native)方法服务的。
  6. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 的 set 方法来设置参数值。这样可以有效防止 SQL 注入攻击,因为参数值会被当作字符串处理,而不会被解析为 SQL 语句的一部分。
    • **:是字符串替换,MyBatis在处理{}**:是字符串替换,MyBatis 在处理 {} 时,会直接将 替换为变量的值。如果变量的值来自用户输入,且没有进行严格的过滤和验证,就可能导致SQL注入风险。例如,如果SQL语句为“SELECTFROMuserWHEREusername={} 替换为变量的值。如果变量的值来自用户输入,且没有进行严格的过滤和验证,就可能导致 SQL 注入风险。例如,如果 SQL 语句为 “SELECT * FROM user WHERE username = '{username}'”,当用户输入 “admin' OR '1'='1” 时,就会导致恶意 SQL 语句的执行。
  7. Dubbo 的服务调用流程
    • 服务注册:服务提供者启动时,将自己提供的服务注册到注册中心(如 Zookeeper)。注册中心保存了服务提供者的地址、端口、服务接口等信息。
    • 服务订阅:服务消费者启动时,向注册中心订阅自己需要的服务。注册中心会将服务提供者的信息返回给消费者。
    • 服务调用:消费者根据从注册中心获取的服务提供者信息,通过网络通信(如 Netty)调用服务提供者的接口。在调用过程中,Dubbo 会进行负载均衡,从多个服务提供者中选择一个来处理请求。常见的负载均衡策略有随机、轮询、最少活跃调用数等。
    • 监控中心:服务提供者和消费者在调用过程中,会将调用次数、调用时间等监控数据发送到监控中心。监控中心可以对这些数据进行分析和展示,帮助运维人员了解系统的运行状况。
  8. RabbitMQ 保证消息可靠性
    • 消息持久化:通过将队列和消息设置为持久化,RabbitMQ 会将队列和消息保存到磁盘上。当 RabbitMQ 服务器重启后,持久化的队列和消息依然存在,不会丢失。队列持久化通过声明队列时设置 durable 参数为 true 实现;消息持久化通过设置 MessageProperties 的 DELIVERY_MODE_PERSISTENT 标志实现。
    • 确认机制(publisher confirm):生产者发送消息后,RabbitMQ 会给生产者返回一个确认消息,告知生产者消息是否成功到达 Broker。生产者可以通过实现 ConfirmCallback 接口来处理确认结果。如果消息发送失败,生产者可以进行重试等操作。
    • 事务机制:生产者可以通过开启事务来确保消息的可靠发送。在事务模式下,生产者发送消息后,调用 channel.txCommit() 方法提交事务,如果提交成功,说明消息已成功发送到 Broker;如果提交失败,可以调用 channel.txRollback() 方法回滚事务,重新发送消息。但事务机制会严重影响性能,不建议在高并发场景下使用。
    • 消费者确认(acknowledgment):消费者接收消息后,需要向 RabbitMQ 发送确认消息(ack),告知 RabbitMQ 消息已被成功处理。如果 RabbitMQ 在一定时间内没有收到消费者的确认消息,会认为消息处理失败,将消息重新放入队列,以便重新分发给其他消费者或再次发送给该消费者。消费者可以通过设置 autoAck 参数来决定是否自动确认消息,建议设置为 false,手动进行确认,以确保消息不会丢失。
  9. xxl - job 任务调度框架的核心原理
    • 调度中心:是 xxl - job 的核心组件,负责管理调度任务、执行器管理、任务触发等功能。调度中心基于 Quartz 框架实现任务调度,通过数据库存储任务信息、执行器信息等。
    • 执行器:部署在业务系统中,负责实际执行任务。执行器启动时会向调度中心注册自己,调度中心会保存执行器的地址、端口等信息。
    • 任务触发:调度中心根据任务的调度规则(如 cron 表达式)触发任务,通过网络通信(如 HTTP)将任务发送给对应的执行器。
    • 任务执行:执行器接收到任务后,根据任务类型(如 Java 方法、Shell 脚本等)执行任务,并将执行结果返回给调度中心。调度中心记录任务的执行结果,供用户查看和分析。
    • 故障转移与负载均衡:当某个执行器出现故障时,调度中心会自动将任务分配到其他可用的执行器上,实现故障转移。同时,调度中心可以根据执行器的负载情况,将任务均匀分配到各个执行器上,实现负载均衡。