面试官:请简要介绍一下 Java 中的多线程机制以及它的应用场景。
王铁牛:多线程就是多个线程同时执行嘛。应用场景比如服务器处理多个客户端请求的时候,可以用多线程来提高效率。
面试官:那说说线程池的工作原理和优势。
王铁牛:线程池就是预先创建一些线程,有任务来了就从线程池里拿线程去执行。优势就是避免频繁创建和销毁线程,节省资源。
面试官:好,回答得还不错。接下来问几个关于 JVM 的问题。类加载机制分哪几个阶段?
王铁牛:嗯……好像有加载、验证、准备、解析、初始化。
面试官:那说说 JVM 内存模型中堆和栈的区别。
王铁牛:堆是存放对象实例的,栈是存放局部变量和方法调用的。
面试官:不错。进入第二轮提问。讲讲 HashMap 的底层数据结构和扩容机制。
王铁牛:HashMap 底层是数组加链表,扩容就是数组大小翻倍。
面试官:ArrayList 如何实现动态扩容?
王铁牛:就是数组满了之后创建一个更大的数组,把原来的数据拷贝过去。
面试官:Spring 框架中依赖注入的方式有哪些?
王铁牛:有构造器注入、setter 注入。
面试官:进入第三轮。说说 Spring Boot 的自动配置原理。
王铁牛:这个……不太清楚,瞎猜一个,是不是根据配置文件自动配置一些组件啥的。
面试官:MyBatis 的缓存机制了解吗?
王铁牛:好像有一级缓存和二级缓存,一级缓存是 SqlSession 级别的,二级缓存是 mapper 级别的。
面试官:Dubbo 的集群容错策略有哪些?
王铁牛:这个真不知道。
面试官:好,面试就到这里,回去等通知吧。
答案:
- 多线程机制及应用场景:
- 多线程机制:多线程是指程序中包含多个执行流,这些执行流可以同时运行。在 Java 中,通过
Thread类或实现Runnable接口来创建线程。 - 应用场景:在服务器端,当有多个客户端同时请求服务时,使用多线程可以让服务器同时处理多个请求,提高服务器的并发处理能力。比如电商网站的订单处理系统,多个用户同时下单,就可以用多线程来同时处理这些订单请求,加快订单处理速度,提升用户体验。
- 多线程机制:多线程是指程序中包含多个执行流,这些执行流可以同时运行。在 Java 中,通过
- 线程池的工作原理和优势:
- 工作原理:线程池预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。线程执行完任务后,不会立即销毁,而是放回线程池等待下一个任务。线程池维护着一个任务队列,当线程池中的线程都在忙碌时,新提交的任务会被放入任务队列中排队等待执行。
- 优势:
- 避免频繁创建和销毁线程:创建和销毁线程是比较消耗系统资源的操作,线程池可以复用已有的线程,减少资源消耗。
- 提高响应速度:因为有线程池中的线程可以快速获取,所以新任务可以更快地得到执行,提高了系统的响应速度。
- 便于管理:可以对线程池中的线程数量、任务队列大小等进行统一管理和配置,更好地适应不同的业务场景。
- JVM 类加载机制的阶段:
- 加载:将类的字节码文件加载到内存中,并创建一个
java.lang.Class对象。 - 验证:检查加载的字节码文件是否符合 JVM 的规范,确保其安全性。
- 准备:为类的静态变量分配内存,并设置默认初始值。
- 解析:将符号引用转换为直接引用,比如将类名、方法名等符号引用解析为具体的内存地址。
- 初始化:执行类的静态代码块、为静态变量赋初始值等操作,真正开始类的初始化过程。
- 加载:将类的字节码文件加载到内存中,并创建一个
- JVM 内存模型中堆和栈的区别:
- 堆:是 JVM 中最大的一块内存区域,用于存放对象实例。堆是线程共享的,多个线程可以访问堆中的对象。堆内存由垃圾回收器自动管理,负责回收不再使用的对象所占用的内存空间。
- 栈:每个线程都有自己独立的栈空间。栈中主要存放局部变量和方法调用的上下文信息。当一个方法被调用时,会在栈中创建一个栈帧,用于存储该方法的局部变量和方法调用的相关信息。方法执行完毕后,对应的栈帧会被销毁。栈的内存分配和回收由系统自动完成,速度比堆快。
- HashMap 的底层数据结构和扩容机制:
- 底层数据结构:HashMap 底层是数组 + 链表 + 红黑树。在 JDK 1.8 之前,当链表长度大于 8 且数组长度大于 64 时,链表会转换为红黑树,以提高查询效率。
- 扩容机制:当 HashMap 中的元素个数超过数组大小 * 加载因子(默认 0.75)时,就会触发扩容。扩容时,会创建一个新的更大的数组,将原数组中的元素重新计算哈希值后放入新数组中。如果链表节点的哈希值在新数组中的位置发生变化,可能会导致链表结构的调整。
- ArrayList 实现动态扩容的方式: 当 ArrayList 中的元素个数超过当前数组的容量时,会创建一个新的更大的数组,新数组的容量通常是原数组容量的 1.5 倍(如果原数组容量小于 64)或 2 倍(如果原数组容量大于等于 64)。然后将原数组中的所有元素逐一复制到新数组中,最后将新数组赋值给原来的数组引用,这样就实现了动态扩容。
- Spring 框架中依赖注入的方式:
- 构造器注入:通过构造函数来注入依赖对象。这种方式在对象创建时就完成了依赖注入,确保对象创建后所有依赖都已注入,不会出现空指针异常。例如:
public class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao = userDao; } }- setter 注入:通过 setter 方法来注入依赖对象。这种方式灵活性较高,可以在对象创建后再进行依赖注入,也可以在运行时动态改变依赖对象。例如:
public class UserService { private UserDao userDao; public void setUserDao(UserDao userDao) { this.userDao = userDao; } } - Spring Boot 的自动配置原理:
Spring Boot 通过
@EnableAutoConfiguration注解开启自动配置功能。它会根据项目中引入的依赖,自动猜测并配置适合的组件。Spring Boot 有一个自动配置类,这些自动配置类会根据条件进行配置。比如项目中引入了数据库相关的依赖,Spring Boot 会自动配置数据源、事务管理器等。自动配置类会通过@Conditional注解来判断条件是否满足,如果满足条件就会将相应的配置生效。同时,Spring Boot 还提供了一些配置属性类,用于读取配置文件中的属性值,以定制自动配置的行为。 - MyBatis 的缓存机制:
- 一级缓存:是 SqlSession 级别的缓存。在同一个 SqlSession 中,当执行相同的 SQL 语句时,会直接从缓存中获取结果,而不会再次查询数据库。一级缓存的生命周期是 SqlSession 的生命周期,当 SqlSession 关闭时,一级缓存也会被清空。
- 二级缓存:是 mapper 级别的缓存。多个 SqlSession 可以共享二级缓存。当一个 SqlSession 执行了某个 mapper 的 SQL 语句后,会将查询结果缓存到二级缓存中。其他 SqlSession 在执行相同 mapper 的 SQL 语句时,如果二级缓存中有对应的结果,就可以直接从缓存中获取。二级缓存的生命周期比较长,直到应用程序重启才会清空。要使用二级缓存,需要在 mapper 配置文件中开启
<cache>标签。
- Dubbo 的集群容错策略:
- Failover Cluster:失败自动切换,当调用失败后,自动切换到其他服务器进行重试。
- Failfast Cluster:快速失败,只发起一次调用,失败立即报错,不进行重试。
- Failsafe Cluster:失败安全,调用失败后,直接忽略,不抛出异常,常用于写操作。
- Failback Cluster:失败自动恢复,调用失败后,会在后台定时重试。
- Forking Cluster:并行调用多个服务器,只要有一个成功就返回,常用于实时性要求较高的场景。