第一轮面试 面试官:首先问几个基础问题。Java 中多线程实现方式有哪些? 王铁牛:可以通过继承 Thread 类,或者实现 Runnable 接口来实现多线程。 面试官:回答得不错。那线程池有什么作用呢? 王铁牛:线程池能复用线程,减少线程创建和销毁的开销,提高系统性能。 面试官:很好。HashMap 在 JDK1.7 和 JDK1.8 中有什么主要区别? 王铁牛:1.8 中引入了红黑树,当链表长度超过阈值会转为红黑树,提高查找效率,1.7 只有链表。
第二轮面试 面试官:接下来深入一些。JVM 的内存模型是怎样的,各个区域都有什么作用? 王铁牛:嗯……有堆、栈、方法区,堆是存放对象实例的,栈是线程私有的,方法区存类信息啥的。 面试官:那 Spring 框架中 IOC 和 AOP 分别是什么,有什么作用? 王铁牛:IOC 是控制反转,把对象创建和管理交给 Spring 容器;AOP 是面向切面编程,能实现一些通用功能,像日志、事务啥的。 面试官:Spring Boot 相对于 Spring 有什么优势? 王铁牛:Spring Boot 能快速搭建项目,自动配置,减少很多样板代码。
第三轮面试 面试官:现在问几个更难的。Dubbo 框架的原理是什么,在分布式系统中有什么作用? 王铁牛:Dubbo 好像是做服务治理的,能实现服务的注册、发现啥的,具体原理不太清楚。 面试官:RabbitMQ 在高并发场景下如何保证消息不丢失? 王铁牛:嗯……好像可以设置持久化,还有确认机制啥的,具体不太明白。 面试官:xxl - job 是如何实现分布式任务调度的? 王铁牛:这个……不太了解。
面试官:好的,今天的面试就到这里。你对今天自己的表现应该也有一定认识,在基础知识方面掌握得还算可以,但对于一些进阶和深入的知识点理解还不够透彻。回去等通知吧,我们会综合评估所有候选人后,再决定是否录用你。
问题答案:
- 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();
}
}
- 线程池的作用:
- 减少资源开销:线程的创建和销毁需要消耗系统资源,线程池可以复用已创建的线程,避免频繁创建和销毁线程带来的开销。
- 提高响应速度:当有新任务到来时,无需等待线程创建,直接从线程池中获取线程执行任务,提高了系统的响应速度。
- 控制并发数量:可以设置线程池的最大线程数等参数,控制系统的并发线程数量,避免过多线程导致系统资源耗尽。
- HashMap 在 JDK1.7 和 JDK1.8 中的主要区别:
- 数据结构:JDK1.7 中 HashMap 采用数组 + 链表的数据结构。JDK1.8 中当链表长度超过阈值(默认为 8)时,链表会转化为红黑树,即采用数组 + 链表 + 红黑树的数据结构。这使得在哈希冲突较多时,查找效率从 O(n)提升到 O(logn)。
- 插入方式:JDK1.7 采用头插法,新元素会插入到链表头部。JDK1.8 采用尾插法,新元素插入到链表尾部,这样在多线程环境下不易形成环形链表导致死循环。
- 扩容机制:JDK1.7 扩容时,需要重新计算每个元素的哈希值并重新插入到新的数组位置。JDK1.8 中,当数组容量小于 64 时,链表长度超过阈值会先进行扩容而不是转化为红黑树;扩容时,部分元素的位置不需要重新计算哈希值,直接根据原索引和新容量计算新位置,提高了扩容效率。
- JVM 的内存模型及各区域作用:
- 堆(Heap):是 JVM 中最大的一块内存区域,被所有线程共享。用于存放对象实例,几乎所有的对象实例都在这里分配内存。堆可以分为新生代和老年代,新生代又分为 Eden 区和两个 Survivor 区。对象首先在 Eden 区分配,当 Eden 区满时,触发 Minor GC,存活的对象会被移动到 Survivor 区,在 Survivor 区经过多次 GC 后,依然存活的对象会被移动到老年代。
- 栈(Stack):每个线程都有自己的栈,是线程私有的。栈中存放栈帧,每个方法被调用时会创建一个栈帧,栈帧中包含局部变量表、操作数栈、动态链接、方法出口等信息。当方法调用结束,栈帧被弹出。
- 方法区(Method Area):被所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK8 及以后,方法区被元空间(Metaspace)取代,元空间使用本地内存。
- Spring 框架中 IOC 和 AOP:
- IOC(控制反转):是一种设计思想,将对象的创建和管理控制权从应用程序代码转移到 Spring 容器。通过依赖注入(Dependency Injection,DI)实现,有构造函数注入、Setter 方法注入等方式。例如,一个类 A 依赖类 B,传统方式是在 A 类中自己创建 B 的实例,而在 Spring 中,由 Spring 容器创建 B 的实例并注入到 A 中。这样可以降低代码的耦合度,提高代码的可维护性和可测试性。
- AOP(面向切面编程):是对 OOP(面向对象编程)的补充。它将一些通用功能(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,形成一个个切面(Aspect)。在运行时,这些切面会在合适的时机(如方法调用前后)织入到业务逻辑中。例如,通过 AOP 可以在方法执行前记录日志,在方法执行后进行事务提交等操作,而不需要在每个业务方法中重复编写这些代码。
- Spring Boot 相对于 Spring 的优势:
- 快速搭建项目:Spring Boot 提供了大量的 Starter 依赖,通过简单的配置就能快速搭建一个完整的项目,减少了手动配置各种依赖和组件的繁琐过程。例如,引入 spring - boot - starter - web 依赖就能快速搭建一个 Web 项目。
- 自动配置:Spring Boot 能根据项目的依赖自动配置 Spring 应用,开发者无需手动编写大量的配置文件。比如,引入 MySQL 依赖后,Spring Boot 会自动配置数据源、JdbcTemplate 等相关组件。
- 减少样板代码:Spring Boot 简化了 Spring 应用的开发,减少了很多样板代码,使开发者能更专注于业务逻辑的实现。
- Dubbo 框架的原理及在分布式系统中的作用:
- 原理:Dubbo 采用微内核 + 插件的架构。核心功能包括服务注册与发现、负载均衡、远程调用等。服务提供者将服务注册到注册中心(如 Zookeeper),服务消费者从注册中心获取服务提供者的地址列表。当消费者调用服务时,通过负载均衡算法从地址列表中选择一个提供者进行远程调用。Dubbo 支持多种远程调用协议,如 Dubbo 协议、HTTP 协议等。
- 在分布式系统中的作用:
- 服务治理:实现服务的注册、发现、监控等功能,使分布式系统中的服务能够有序管理。
- 提高系统可扩展性:通过将业务拆分成多个服务,每个服务可以独立部署和扩展,提高了整个系统的可扩展性。
- 负载均衡:在多个服务提供者之间进行负载均衡,提高系统的性能和可用性。
- RabbitMQ 在高并发场景下保证消息不丢失的方法:
- 生产者端:
- 开启 confirm 模式:生产者将信道设置为 confirm 模式后,当消息成功到达 Broker 时,Broker 会发送一个确认消息给生产者。生产者可以通过监听确认消息来判断消息是否发送成功,若未收到确认消息则进行消息重发。
- 事务机制:生产者可以通过开启事务来确保消息发送成功。在事务模式下,生产者发送消息后,通过提交事务来确认消息发送,如果事务提交失败则回滚事务并重发消息。但事务机制会严重影响性能,不适合高并发场景。
- Broker 端:
- 消息持久化:将队列和消息都设置为持久化。队列持久化保证队列在 Broker 重启后依然存在,消息持久化保证消息在 Broker 重启后不会丢失。
- 消费者端:
- 手动确认机制:消费者设置为手动确认模式,在接收到消息并处理完成后,向 Broker 发送确认消息。如果消费者在处理消息过程中出现异常未发送确认消息,Broker 会认为消息未被成功消费,会将消息重新投递给其他消费者或在一定时间后再次投递给该消费者。
- 生产者端:
- xxl - job 实现分布式任务调度的原理:
- 调度中心:是整个调度系统的核心,负责任务的管理、调度配置、任务触发等。它基于数据库存储任务信息和调度日志等数据。
- 执行器:部署在各个业务系统中,负责接收调度中心的调度请求并执行任务。执行器与调度中心通过 HTTP 进行通信。
- 任务注册:执行器启动时,会向调度中心注册自己,并汇报自己能执行的任务列表。
- 任务调度:调度中心根据任务的配置(如执行周期、触发时间等),定时触发任务,并将任务调度请求发送给对应的执行器。
- 任务执行与结果反馈:执行器接收到任务调度请求后,执行任务,并将任务执行结果反馈给调度中心。调度中心记录任务执行结果和日志,方便用户查看和监控。