面试官:请简要介绍一下 Java 中的多线程,包括线程的创建方式和线程安全问题。
王铁牛:线程创建方式有继承 Thread 类和实现 Runnable 接口。线程安全问题嘛,就是要注意同步,用 synchronized 关键字。
面试官:嗯,回答得还不错。那说说在多线程环境下,如何保证数据的一致性?
王铁牛:可以用锁机制,像 synchronized 或者 ReentrantLock,也可以用 volatile 关键字修饰变量。
面试官:很好。再问一个,讲讲线程池的原理和使用场景。
王铁牛:线程池就是预先创建一些线程,复用这些线程来执行任务。使用场景嘛,就是大量并发任务的时候。
第一轮结束
面试官:接下来问关于 JVM 的问题。类加载机制分哪几个阶段?
王铁牛:嗯……好像是加载、验证、准备、解析、初始化。
面试官:对。那说说 Java 内存区域都有哪些?
王铁牛:有堆、栈、方法区、程序计数器、本地方法栈。
面试官:讲讲垃圾回收算法有哪些?
王铁牛:什么标记清除、标记整理、复制算法吧。
第二轮结束
面试官:最后一轮,说说 Spring 框架中 IoC 和 AOP 的概念。
王铁牛:IoC 就是控制反转,AOP 就是面向切面编程。
面试官:Spring Boot 与传统 Spring 相比有哪些优势?
王铁牛:Spring Boot 更简单,自动配置多,上手快。
面试官:说说 MyBatis 的缓存机制。
王铁牛:有一级缓存和二级缓存,一级缓存是 SqlSession 级别的,二级缓存是 mapper 级别的。
面试结束
面试官让王铁牛回家等通知。
答案:
- 多线程:
- 线程创建方式:
- 继承 Thread 类:通过继承 Thread 类并重写其 run 方法来创建线程。例如:
class MyThread extends Thread { @Override public void run() { System.out.println("Thread is running"); } } MyThread thread = new MyThread(); thread.start(); - 实现 Runnable 接口:实现 Runnable 接口的类必须实现其 run 方法,然后将该类的实例作为参数传递给 Thread 类的构造函数来创建线程。例如:
class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable is running"); } } Thread thread = new Thread(new MyRunnable()); thread.start();
- 继承 Thread 类:通过继承 Thread 类并重写其 run 方法来创建线程。例如:
- 线程安全问题:
- 同步机制:
- synchronized 关键字:可以修饰方法或代码块。当一个方法或代码块被 synchronized 修饰时,同一时刻只能有一个线程访问它。例如:
public synchronized void synchronizedMethod() { // 线程安全的代码 } - ReentrantLock:这是一个可重入的互斥锁,比 synchronized 更加灵活。例如:
import java.util.concurrent.locks.ReentrantLock; class MyClass { private ReentrantLock lock = new ReentrantLock(); public void lockMethod() { lock.lock(); try { // 线程安全的代码 } finally { lock.unlock(); } } }
- synchronized 关键字:可以修饰方法或代码块。当一个方法或代码块被 synchronized 修饰时,同一时刻只能有一个线程访问它。例如:
- volatile 关键字:用于修饰变量,保证变量的可见性。当一个变量被声明为 volatile 时,它会保证对该变量的写操作会立即刷新到主内存中,而读操作会从主内存中读取最新的值。例如:
volatile int value;
- 同步机制:
- 线程创建方式:
- 线程池:
- 原理:线程池预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务,任务执行完毕后线程不会销毁,而是继续留在线程池中等待下一个任务。这样可以避免频繁创建和销毁线程带来的开销。
- 使用场景:适用于大量并发任务的场景,比如服务器端处理大量的客户端请求。例如在一个 Web 应用中,当有大量用户同时访问时,使用线程池可以高效地处理这些请求,避免线程资源的过度消耗。
- JVM 类加载机制:
- 加载:将类的字节码文件加载到内存中。
- 验证:检查加载的字节码文件是否符合 JVM 的规范。
- 准备:为类的静态变量分配内存并设置初始值。
- 解析:将符号引用转换为直接引用。
- 初始化:执行类的静态代码块和为静态变量赋值。
- Java 内存区域:
- 堆:是 Java 程序运行时的内存区域,用于存储对象实例。
- 栈:每个线程都有自己的栈,用于存储局部变量和方法调用的上下文。
- 方法区:存储类的信息、常量、静态变量等。
- 程序计数器:记录当前线程执行的字节码指令的地址。
- 本地方法栈:用于执行本地方法(用 C 或 C++ 实现的方法)。
- 垃圾回收算法:
- 标记清除算法:先标记出所有需要回收的对象,然后统一回收这些对象所占用的内存空间。这种算法的缺点是会产生大量不连续的内存碎片。
- 标记整理算法:在标记清除算法的基础上,在清除对象后,将存活的对象向内存空间的一端移动,然后清除边界以外的内存。
- 复制算法:将内存空间分为两块,每次只使用其中一块。当这一块内存用完后,将存活的对象复制到另一块内存上,然后清除原来的那一块内存。这种算法适用于对象存活率比较低的情况。
- Spring 的 IoC(控制反转):
- 传统的对象创建和依赖管理由程序员手动完成,而 IoC 容器负责创建对象、管理对象之间的依赖关系。例如,在一个业务类中需要依赖另一个服务类,传统方式是在业务类中手动创建服务类的实例。而在 Spring 中,通过配置文件或注解,将服务类的创建和注入交给 Spring 容器来完成。这样,业务类只需要声明依赖,不需要关心具体的创建过程,实现了控制权的反转。
- Spring 的 AOP(面向切面编程):
- AOP 允许将一些横切关注点(如日志记录、事务管理等)与业务逻辑分离。通过 AOP,可以在不修改业务逻辑代码的情况下,为业务方法添加额外的功能。例如,使用 AOP 实现日志记录,只需要定义一个切面,在切面中编写日志记录的逻辑,然后通过切点指定哪些业务方法需要应用这个切面。当业务方法执行时,会自动执行切面中的日志记录逻辑。
- Spring Boot 与传统 Spring 相比的优势:
- 自动配置:Spring Boot 提供了大量的自动配置,能够根据项目中引入的依赖自动配置相关的 Bean 和配置,大大减少了手动配置的工作量。
- 快速上手:基于约定大于配置的原则,项目结构和配置更加简单,开发人员可以更快地搭建起一个 Spring 应用。
- 内置服务器:Spring Boot 内置了 Tomcat 等服务器,无需像传统 Spring 那样手动配置和部署服务器。
- MyBatis 的缓存机制:
- 一级缓存:是 SqlSession 级别的缓存。当在同一个 SqlSession 中执行相同的 SQL 语句时,会直接从一级缓存中获取结果,而不会再次查询数据库。例如:
SqlSession sqlSession = sqlSessionFactory.openSession(); User user1 = sqlSession.selectOne("selectUserById", 1); User user2 = sqlSession.selectOne("selectUserById", 1); // 从一级缓存中获取结果 sqlSession.close(); - 二级缓存:是 mapper 级别的缓存。多个 SqlSession 可以共享二级缓存。开启二级缓存后,当不同的 SqlSession 执行相同的 SQL 语句时,会先从二级缓存中查找结果。例如:
然后在代码中使用:<mapper namespace="com.example.mapper.UserMapper"> <cache /> <select id="selectUserById" parameterType="int" resultType="User"> SELECT * FROM user WHERE id = #{id} </select> </mapper>SqlSession sqlSession1 = sqlSessionFactory.openSession(); User user1 = sqlSession1.selectOne("selectUserById", 1); sqlSession1.close(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); User user2 = sqlSession2.selectOne("selectUserById", 1); // 从二级缓存中获取结果 sqlSession2.close();
- 一级缓存:是 SqlSession 级别的缓存。当在同一个 SqlSession 中执行相同的 SQL 语句时,会直接从一级缓存中获取结果,而不会再次查询数据库。例如: