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

38 阅读12分钟

第一轮面试 面试官:首先问几个基础问题。Java 中多线程创建有几种方式? 王铁牛:嗯,有继承 Thread 类,还有实现 Runnable 接口这两种方式。 面试官:不错,回答得很准确。那 ArrayList 和 HashMap 在存储数据结构上有什么不同? 王铁牛:ArrayList 是基于数组结构,能顺序存储数据;HashMap 是基于哈希表,通过 key - value 形式存储。 面试官:回答得很好。那 Spring 框架中 IOC 是什么? 王铁牛:IOC 就是控制反转,把对象创建和管理的控制权交给 Spring 容器。 面试官:非常好,基础掌握得挺扎实。

第二轮面试 面试官:接下来深入一些。JUC 包下的 CountDownLatch 是做什么的? 王铁牛:呃,好像是用来控制线程等待的,具体怎么用不太清楚了。 面试官:那线程池的核心参数有哪些,分别有什么作用? 王铁牛:有核心线程数、最大线程数,还有个队列容量吧,核心线程数就是一开始创建的线程数,最大线程数就是最多能创建的线程数,队列容量就是存放任务的地方。 面试官:回答得不是很清晰,队列容量具体是哪个队列,不同队列策略有什么不同呢?再说说 Spring Boot 自动配置原理。 王铁牛:就是 Spring Boot 能自动配置一些东西,根据引入的依赖,好像是有个自动配置类。 面试官:嗯,回答比较模糊,没有深入到原理层面。

第三轮面试 面试官:继续深入。MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 能防止 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 类的构造函数,再调用 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();
    }
}
  • 实现 Callable 接口:创建一个类实现 Callable 接口,实现 call 方法。通过 FutureTask 类包装 Callable 实现类的实例,再将 FutureTask 作为参数传递给 Thread 类的构造函数启动线程。与前两种方式不同的是,Callable 接口的 call 方法可以有返回值,并且可以抛出异常。例如:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 100;
    }
}
public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        Integer result = futureTask.get();
        System.out.println("线程返回结果:" + result);
    }
}
  1. ArrayList 和 HashMap 在存储数据结构上有什么不同?
    • ArrayList:基于动态数组实现,它按照顺序存储元素,元素在内存中是连续存储的。这使得它在随机访问时效率很高,因为可以通过索引直接定位到元素位置。例如 list.get(index) 操作时间复杂度为 O(1)。但在插入和删除元素时,如果不是在末尾操作,需要移动元素,时间复杂度为 O(n)。
    • HashMap:基于哈希表实现,以 key - value 对的形式存储数据。它通过对 key 进行哈希运算来确定元素的存储位置,从而实现快速的查找。理想情况下,查找、插入和删除操作的时间复杂度都接近 O(1)。但在哈希冲突较多时,性能会下降,此时哈希表会通过链表或红黑树(JDK 1.8 及以后,当链表长度超过 8 且数组容量大于等于 64 时,链表会转换为红黑树)来解决冲突。
  2. Spring 框架中 IOC 是什么?
    • 控制反转(Inversion of Control):是 Spring 框架的核心概念之一。传统的应用程序中,对象的创建和管理由应用程序自身负责,这导致对象之间的耦合度较高。而在 Spring 中,IOC 把对象创建和管理的控制权从应用程序代码转移到了 Spring 容器。例如,在一个业务逻辑类中,如果需要依赖另一个类的实例,传统方式是在业务逻辑类中自己创建依赖类的实例,如 UserService userService = new UserServiceImpl();。而在 Spring 中,通过配置(XML 配置或注解配置)将依赖类交给 Spring 容器管理,业务逻辑类只需要声明依赖,Spring 容器会在运行时将依赖的实例注入进来,如使用注解 @Autowired 自动注入 UserService 实例。这样使得代码的可维护性和可测试性大大提高,降低了对象之间的耦合度。
  3. JUC 包下的 CountDownLatch 是做什么的?
    • CountDownLatch:是 JUC(java.util.concurrent)包下的一个同步工具类。它允许一个或多个线程等待,直到其他一组线程完成操作。它内部维护一个计数器,通过构造函数初始化计数器的值。例如 CountDownLatch latch = new CountDownLatch(3); 表示计数器初始值为 3。当调用 latch.countDown() 方法时,计数器减 1。而调用 latch.await() 方法的线程会被阻塞,直到计数器的值变为 0。常用于以下场景:比如有一个主线程需要等待多个子线程完成任务后再继续执行,就可以在主线程中创建一个 CountDownLatch,子线程执行完任务后调用 countDown(),主线程调用 await() 等待所有子线程完成。
  4. 线程池的核心参数有哪些,分别有什么作用?
    • 核心线程数(corePoolSize):线程池中会一直存活的线程数量,即使这些线程处于空闲状态,也不会被销毁(除非设置了 allowCoreThreadTimeOut 为 true)。当有新任务提交到线程池时,如果当前线程数小于 corePoolSize,会创建新的线程来处理任务。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数量。当任务队列已满,且当前线程数小于 maximumPoolSize 时,线程池会创建新的线程来处理任务。
    • 队列容量(workQueue):用于存放等待执行任务的队列。当线程池中的线程数达到 corePoolSize 后,新提交的任务会被放入这个队列中等待执行。常见的队列类型有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列,在不设置容量时为无界)、SynchronousQueue(同步队列,不存储任务,直接提交给线程处理)等。不同队列策略会影响线程池的性能和行为,例如 ArrayBlockingQueue 有界,当队列满时,新任务可能会触发创建新线程(如果当前线程数小于 maximumPoolSize);而 LinkedBlockingQueue 无界时,可能会导致任务堆积,占用大量内存。
    • 线程存活时间(keepAliveTime):当线程池中的线程数量超过 corePoolSize 时,多余的空闲线程在等待新任务到来的这段时间内,如果超过 keepAliveTime 还没有新任务,这些线程会被销毁,直到线程池中的线程数量不超过 corePoolSize。
    • 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 拒绝策略(RejectedExecutionHandler):当线程池无法处理新提交的任务时(队列已满且线程数达到 maximumPoolSize),会执行拒绝策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(将任务交给调用者线程执行)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交新任务)。
  5. Spring Boot 自动配置原理
    • Spring Boot 自动配置:是基于 Spring 框架的条件化配置实现的。Spring Boot 启动时,会扫描所有依赖的 jar 包中的 META - INF/spring.factories 文件。在这个文件中,定义了各种自动配置类,例如 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\...。这些自动配置类会根据项目的依赖和配置条件进行加载。例如,如果项目中引入了 spring - data - jpa 依赖,Spring Boot 会自动配置 JPA 相关的组件,包括数据源、实体管理器等。自动配置类上通常会使用 @Conditional 系列注解,如 @ConditionalOnClass(当类路径下存在某个类时才生效)、@ConditionalOnProperty(当配置文件中某个属性满足条件时才生效)等。通过这些条件判断,Spring Boot 可以根据项目实际情况自动配置合适的组件,大大简化了 Spring 应用的配置过程。
  6. MyBatis 中 #{} 和 ${} 的区别是什么?
    • #{}:是预编译处理方式。MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ?,然后使用 PreparedStatement 进行参数设置。这种方式可以有效防止 SQL 注入攻击,因为参数是作为字符串传入,而不是直接嵌入 SQL 语句中。例如:SELECT * FROM user WHERE username = #{username},在执行时会变为 SELECT * FROM user WHERE username =?,然后通过 PreparedStatement 的 setString 等方法设置参数值。
    • **:是字符串替换方式。MyBatis在处理{}**:是字符串替换方式。MyBatis 在处理 `{}时,会直接将中的内容替换为实际的值,然后拼接成完整的SQL语句。这种方式存在SQL注入风险,因为如果传入的值包含恶意SQL语句,会直接拼接到SQL中执行。例如:SELECTFROMuserWHEREusername={}` 中的内容替换为实际的值,然后拼接成完整的 SQL 语句。这种方式存在 SQL 注入风险,因为如果传入的值包含恶意 SQL 语句,会直接拼接到 SQL 中执行。例如:`SELECT * FROM user WHERE username = '{username}',如果传入的 username'; DROP TABLE user; --,则会执行恶意的 SQL 语句。所以在使用 ${}` 时,一定要对传入的值进行严格的校验和过滤。
  7. Dubbo 服务调用过程是怎样的?
    • 服务注册:服务提供者启动时,会将自己提供的服务注册到注册中心(如 Zookeeper、Nacos 等)。注册中心记录了服务提供者的地址、端口、服务接口等信息。
    • 服务订阅:服务消费者启动时,会从注册中心订阅自己需要调用的服务。注册中心会将服务提供者的信息推送给服务消费者。
    • 服务调用:服务消费者根据从注册中心获取的服务提供者信息,通过网络通信(如 Netty)调用服务提供者的接口。在调用过程中,Dubbo 会进行负载均衡,从多个服务提供者中选择一个来执行调用。常见的负载均衡策略有随机、轮询、最少活跃调用数等。同时,Dubbo 还支持多种协议进行通信,如 Dubbo 协议、HTTP 协议等。
    • 服务监控:Dubbo 可以集成监控中心(如 Dubbo - Admin),对服务的调用次数、响应时间、调用成功率等指标进行监控,以便及时发现和解决问题。
  8. RabbitMQ 如何保证消息不丢失?
    • 生产者端
      • 开启 confirm 模式:生产者调用 channel.confirmSelect() 开启 confirm 模式。当消息成功到达 RabbitMQ 服务器后,服务器会返回一个确认消息给生产者。生产者可以通过监听 ConfirmListener 来处理确认消息,判断消息是否成功发送。如果未收到确认消息,可以进行消息重发等操作。
      • 事务机制:生产者可以通过 channel.txSelect() 开启事务,在发送消息后调用 channel.txCommit() 提交事务。如果消息发送失败,调用 channel.txRollback() 回滚事务并重发消息。但事务机制会严重影响性能,一般不推荐使用,confirm 模式更为常用。
    • RabbitMQ 服务器端
      • 消息持久化:对队列和消息都设置持久化。队列通过 Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) 方法中的 durable 参数设置为 true 实现持久化,这样队列会在 RabbitMQ 服务器重启后依然存在。消息通过 AMQP.BasicProperties.Builder durable(boolean durable) 方法设置 durabletrue 实现持久化,这样消息会在服务器持久化存储,即使服务器重启也不会丢失。
    • 消费者端
      • 关闭自动确认:消费者通过 channel.basicConsume(String queue, boolean autoAck, String consumerTag, boolean noLocal, boolean exclusive, Map<String, Object> arguments, Consumer callback) 方法中的 autoAck 参数设置为 false,关闭自动确认机制。消费者在处理完消息后,手动调用 channel.basicAck(deliveryTag, boolean multiple) 方法进行确认。如果消费者在处理消息过程中出现异常,未进行确认,RabbitMQ 会认为消息未被成功消费,会重新将消息发送给其他消费者(如果有多个消费者)或重新放入队列等待下一次消费。
  9. xxl - job 调度中心的作用是什么?
  • 任务管理:提供可视化界面,方便用户创建、编辑、删除任务。可以设置任务的执行周期(如 cron 表达式定义的定时任务)、任务参数、任务描述等信息。
  • 任务调度:按照用户设定的执行周期,定时触发任务执行。调度中心负责将任务分配到合适的执行器节点上执行。
  • 执行器管理:管理任务执行器,包括注册执行器、查看执行器状态等。执行器是实际执行任务的模块,调度中心与执行器通过网络通信进行任务调度和结果反馈。
  • 日志管理:记录任务执行的日志信息,包括任务开始时间、结束时间、执行结果、执行过程中的输出等。方便用户排查任务执行过程中出现的问题。
  • 故障转移与重试:当某个执行器节点出现故障时,调度中心可以将任务重新分配到其他正常的执行器节点上执行。对于执行失败的任务,调度中心可以根据配置进行重试,确保任务最终执行成功。