Java 后端面试常考八股
适用目标:Java 后端开发校招、社招初中级面试复习。 复习方式:先掌握“是什么、为什么、怎么用、有什么坑”,再结合项目经历准备追问。
目录
Java 基础
1. Java 有哪些特点?
Java 是一种面向对象、跨平台、自动内存管理的语言。常见特点包括:
-
跨平台:通过 JVM 实现“一次编译,到处运行”。
-
面向对象:封装、继承、多态。
-
自动内存管理:通过 GC 回收无用对象。
-
生态完善:Spring、MyBatis、Netty、Kafka 等中间件生态成熟。
-
多线程支持:语言层面提供线程、锁、并发容器和线程池。
2. JDK、JRE、JVM 的区别?
-
JVM:Java 虚拟机,负责执行字节码。
-
JRE:Java 运行环境,包含 JVM 和运行时类库。
-
JDK:Java 开发工具包,包含 JRE、编译器
javac、调试工具等。
开发需要 JDK,运行普通 Java 程序只需要 JRE,不过现代 JDK 发行版通常已经将这些能力整合在一起。
3. == 和 equals() 的区别?
-
==:-
基本类型比较值。
-
引用类型比较对象地址。
-
-
equals():-
默认实现等价于
==。 -
很多类会重写,比如
String比较字符内容。
-
常见追问:为什么重写 equals() 必须重写 hashCode()?
因为哈希容器如 HashMap、HashSet 会先用 hashCode() 定位桶,再用 equals() 判断对象是否相等。如果两个对象 equals() 相等,但 hashCode() 不同,会导致集合行为异常。
4. String、StringBuilder、StringBuffer 的区别?
-
String:不可变对象,每次修改都会产生新对象。 -
StringBuilder:可变字符序列,线程不安全,性能较好。 -
StringBuffer:可变字符序列,方法加了synchronized,线程安全,性能略低。
单线程字符串拼接优先用 StringBuilder。多线程共享同一个字符串缓冲区时可考虑 StringBuffer,但实际业务中更常通过局部变量避免共享。
5. final 关键字有什么作用?
-
修饰类:类不能被继承,例如
String。 -
修饰方法:方法不能被子类重写。
-
修饰变量:变量只能赋值一次。
注意:final 修饰引用类型时,引用地址不可变,但对象内部状态仍可能改变。
final List<String> list = new ArrayList<>();
list.add("A"); // 可以
// list = new ArrayList<>(); // 不可以
6. Java 异常体系
Java 异常根类是 Throwable,分为:
-
Error:严重错误,一般不处理,如OutOfMemoryError。 -
Exception:程序可处理异常。-
受检异常:编译期要求处理,如
IOException。 -
非受检异常:运行期异常,如
NullPointerException、IllegalArgumentException。
-
实际开发中,业务异常通常自定义为运行时异常,并通过全局异常处理器统一返回。
7. 深拷贝和浅拷贝
-
浅拷贝:只复制对象本身,引用字段仍指向原对象中的同一引用。
-
深拷贝:对象及其引用字段都复制一份,彼此独立。
实现深拷贝的方式:
-
手动 new 对象并复制字段。
-
序列化 / 反序列化。
-
JSON 转换。
-
使用拷贝构造器或专门的映射工具。
8. 反射是什么?有哪些使用场景?
反射是在运行时获取类信息、创建对象、调用方法、访问字段的机制。
常见场景:
-
Spring IOC 创建 Bean。
-
MyBatis 根据映射关系封装对象。
-
注解解析。
-
动态代理。
-
测试框架。
缺点:
-
性能低于直接调用。
-
破坏封装性。
-
编译期类型检查变弱。
9. 注解是什么?
注解是附加在类、方法、字段等元素上的元数据,本身不直接改变程序逻辑,需要结合反射、编译器、框架或 AOP 使用。
常见元注解:
-
@Target:注解可作用的位置。 -
@Retention:注解保留策略。 -
@Documented:是否生成到文档。 -
@Inherited:是否可被子类继承。
面向对象
1. 封装、继承、多态
-
封装:隐藏内部实现,通过公开方法访问对象状态。
-
继承:子类复用父类属性和方法。
-
多态:父类引用指向子类对象,运行时根据真实对象调用方法。
多态成立条件:
-
有继承或实现关系。
-
子类重写父类方法。
-
父类引用指向子类对象。
2. 重载和重写区别
| 对比项 | 重载 | 重写 |
|---|---|---|
| 发生位置 | 同一个类中 | 父子类之间 |
| 方法名 | 相同 | 相同 |
| 参数列表 | 必须不同 | 必须相同 |
| 返回值 | 可不同,但不能只靠返回值区分 | 子类返回值可协变 |
| 访问权限 | 无特殊要求 | 不能比父类更严格 |
| 绑定时机 | 编译期 | 运行期 |
3. 抽象类和接口区别
| 对比项 | 抽象类 | 接口 |
|---|---|---|
| 设计目的 | 表示“是什么” | 表示“能做什么” |
| 继承数量 | 单继承 | 多实现 |
| 成员变量 | 可有普通变量 | 默认 public static final |
| 方法 | 可有普通方法、抽象方法 | 可有抽象方法、默认方法、静态方法 |
| 构造器 | 有 | 无 |
经验:抽象类用于提取同类对象的公共状态和行为,接口用于定义能力和规范。
4. 创建对象有哪些方式?
-
new -
反射
-
克隆
clone() -
反序列化
-
工厂方法
-
Spring IOC 容器创建
集合框架
1. Java 集合体系
-
Collection-
List:有序、可重复,如ArrayList、LinkedList。 -
Set:无序、不可重复,如HashSet、TreeSet。 -
Queue:队列,如ArrayDeque、PriorityQueue。
-
-
Map-
HashMap -
LinkedHashMap -
TreeMap -
ConcurrentHashMap
-
2. ArrayList 和 LinkedList 区别
| 对比项 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | 快,O(1) |
慢,O(n) |
| 头尾插入删除 | 可能移动元素 | 较快 |
| 内存占用 | 较低 | 较高,需要存前后指针 |
| 使用场景 | 查询多 | 插入删除多 |
实际开发中,绝大多数场景优先使用 ArrayList。
3. ArrayList 扩容机制
ArrayList 底层是数组。添加元素时,如果容量不足会扩容,通常扩为原容量的 1.5 倍,然后通过数组复制迁移元素。
优化方式:如果能预估集合大小,创建时指定初始容量。
List<String> list = new ArrayList<>(1000);
4. HashMap 底层原理
JDK 8 中,HashMap 底层结构是:
数组 + 链表 + 红黑树
put 流程:
-
根据 key 的
hashCode()计算 hash。 -
根据 hash 定位数组下标。
-
如果桶为空,直接插入。
-
如果桶不为空,比较 hash 和 key。
-
key 相同则覆盖旧值。
-
key 不同则插入链表或红黑树。
-
元素数量超过阈值触发扩容。
5. 为什么 HashMap 容量是 2 的幂?
主要为了用位运算提高取模效率:
index = (n - 1) & hash;
当容量是 2 的幂时,(n - 1) & hash 等价于 hash % n,并且能让元素分布更均匀。
6. HashMap 什么时候树化?
JDK 8 中,单个桶的链表长度达到 8,且数组容量至少为 64 时,会转为红黑树。
如果数组容量小于 64,优先扩容而不是树化。
7. HashMap 为什么线程不安全?
典型问题:
-
多线程 put 可能导致数据覆盖。
-
扩容期间可能出现数据丢失。
-
并发读写可能读到不一致状态。
并发场景应使用 ConcurrentHashMap。
8. ConcurrentHashMap 原理
JDK 7 使用分段锁 Segment。 JDK 8 使用 CAS + synchronized + volatile,锁粒度降低到桶级别。
JDK 8 put 大致流程:
-
如果数组未初始化,先初始化。
-
如果目标桶为空,用 CAS 插入。
-
如果桶不为空,对桶头节点加
synchronized。 -
插入链表或红黑树。
-
必要时协助扩容。
9. HashMap、LinkedHashMap、TreeMap
-
HashMap:无序,查询快。 -
LinkedHashMap:维护插入顺序或访问顺序,适合实现 LRU。 -
TreeMap:基于红黑树,按 key 排序。
JVM
1. JVM 运行时内存区域
JVM 主要内存区域:
-
程序计数器:记录当前线程执行的字节码行号。
-
Java 虚拟机栈:存放栈帧、局部变量表、操作数栈等。
-
本地方法栈:服务 native 方法。
-
堆:存放对象实例,是 GC 主要区域。
-
方法区:存放类元信息、常量、静态变量等。
JDK 8 后,永久代被元空间 Metaspace 替代,元空间使用本地内存。
2. 堆和栈的区别
| 对比项 | 堆 | 栈 |
|---|---|---|
| 存储内容 | 对象实例 | 方法调用、局部变量 |
| 线程共享 | 共享 | 线程私有 |
| 生命周期 | 由 GC 管理 | 随方法调用自动入栈出栈 |
| 异常 | OutOfMemoryError |
StackOverflowError |
3. 判断对象是否可回收
主流 JVM 使用可达性分析算法。
从 GC Roots 出发,能被直接或间接引用的对象是可达对象,不可达对象可被回收。
常见 GC Roots:
-
虚拟机栈中引用的对象。
-
方法区中类静态属性引用的对象。
-
方法区中常量引用的对象。
-
本地方法栈中 JNI 引用的对象。
-
活跃线程对象。
4. 常见垃圾回收算法
-
标记-清除:先标记可回收对象,再清除。缺点是产生内存碎片。
-
标记-复制:将存活对象复制到另一块区域。适合新生代。
-
标记-整理:标记后移动存活对象,整理内存。适合老年代。
-
分代收集:根据对象生命周期分新生代、老年代采用不同算法。
5. 新生代和老年代
堆通常分为:
-
新生代:对象创建后先进入这里,生命周期短。
-
Eden
-
Survivor 0
-
Survivor 1
-
-
老年代:长期存活对象进入这里。
对象晋升到老年代的情况:
-
年龄达到阈值。
-
大对象直接进入老年代。
-
Survivor 空间不足。
-
动态年龄判断。
6. Minor GC、Major GC、Full GC
-
Minor GC:回收新生代。
-
Major GC:通常指回收老年代,不同语境可能不同。
-
Full GC:回收整个堆和方法区,代价较高。
频繁 Full GC 常见原因:
-
老年代空间不足。
-
元空间不足。
-
大对象频繁创建。
-
内存泄漏。
-
显式调用
System.gc()。
7. 常见垃圾收集器
-
Serial:单线程,适合小内存客户端场景。
-
ParNew:Serial 多线程版本。
-
Parallel Scavenge:关注吞吐量。
-
CMS:关注低停顿,已逐渐被替代。
-
G1:面向服务端,兼顾吞吐和低停顿。
-
ZGC / Shenandoah:低延迟收集器。
8. 类加载过程
类加载过程:
-
加载:读取字节码生成
Class对象。 -
验证:校验字节码安全性。
-
准备:为静态变量分配内存并设置默认值。
-
解析:符号引用转直接引用。
-
初始化:执行静态变量赋值和静态代码块。
9. 双亲委派模型
类加载器收到加载请求后,先委托父加载器加载,父加载器无法加载时才由自己加载。
优点:
-
避免类重复加载。
-
保护 Java 核心类库安全。
典型破坏场景:
-
JDBC SPI。
-
Tomcat 多应用隔离。
-
OSGi、热部署框架。
10. JVM 调优常用参数
-Xms512m # 初始堆大小 -Xmx512m # 最大堆大小 -Xmn256m # 新生代大小 -XX:+UseG1GC # 使用 G1 -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof
排查工具:
-
jps:查看 Java 进程。 -
jstack:查看线程栈。 -
jmap:查看堆信息、导出 dump。 -
jstat:查看 GC 统计。 -
Arthas:线上诊断常用工具。
并发编程
1. 进程和线程区别
-
进程:资源分配的基本单位,拥有独立地址空间。
-
线程:CPU 调度的基本单位,共享进程资源。
线程切换开销小,但共享内存带来线程安全问题。
2. 创建线程的方式
-
继承
Thread。 -
实现
Runnable。 -
实现
Callable,配合FutureTask。 -
使用线程池。
实际开发中优先使用线程池,避免频繁创建销毁线程。
3. 线程生命周期
Java 线程状态:
-
NEW -
RUNNABLE -
BLOCKED -
WAITING -
TIMED_WAITING -
TERMINATED
4. sleep() 和 wait() 区别
| 对比项 | sleep | wait |
|---|---|---|
| 所属类 | Thread |
Object |
| 是否释放锁 | 不释放 | 释放 |
| 使用位置 | 任意位置 | 必须在同步块或同步方法中 |
| 唤醒方式 | 时间到自动恢复 | notify() / notifyAll() |
5. synchronized 原理
synchronized 可以修饰方法或代码块,保证同一时刻只有一个线程进入临界区。
底层基于对象监视器 Monitor。进入同步块需要获取 Monitor,退出时释放 Monitor。
JDK 6 后做了大量优化:
-
偏向锁。
-
轻量级锁。
-
重量级锁。
-
锁粗化。
-
锁消除。
6. volatile 的作用
volatile 保证:
-
可见性:一个线程修改变量后,其他线程能及时看到。
-
有序性:禁止特定指令重排序。
volatile 不保证复合操作的原子性。
volatile int count = 0; count++; // 不是原子操作
7. JMM 是什么?
JMM 是 Java 内存模型,用于定义线程之间如何通过主内存进行通信,以及可见性、有序性、原子性规则。
核心概念:
-
主内存。
-
工作内存。
-
happens-before。
常见 happens-before 规则:
-
程序顺序规则。
-
volatile 写先行发生于后续读。
-
解锁先行发生于后续加锁。
-
线程 start 规则。
-
线程 join 规则。
8. CAS 是什么?
CAS 是 Compare And Swap,比较并交换。它是一种乐观锁思想。
CAS 操作包含三个值:
-
内存值 V。
-
预期值 A。
-
新值 B。
只有当 V 等于 A 时,才将 V 更新为 B。
问题:
-
ABA 问题。
-
自旋开销。
-
只能保证单个变量的原子操作。
ABA 可通过版本号解决,例如 AtomicStampedReference。
9. ReentrantLock 和 synchronized 区别
| 对比项 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM | JDK |
| 是否可重入 | 是 | 是 |
| 公平锁 | 不支持 | 支持 |
| 可中断 | 不支持 | 支持 |
| 尝试加锁 | 不支持 | 支持 tryLock() |
| 条件队列 | 一个等待队列 | 多个 Condition |
简单同步优先使用 synchronized,复杂并发控制可使用 ReentrantLock。
10. 线程池核心参数
ThreadPoolExecutor 核心参数:
-
corePoolSize:核心线程数。 -
maximumPoolSize:最大线程数。 -
keepAliveTime:非核心线程空闲存活时间。 -
unit:时间单位。 -
workQueue:任务队列。 -
threadFactory:线程工厂。 -
handler:拒绝策略。
执行流程:
-
线程数小于核心线程数,创建核心线程。
-
核心线程已满,任务进入队列。
-
队列满,创建非核心线程。
-
达到最大线程数,执行拒绝策略。
11. 常见拒绝策略
-
AbortPolicy:抛异常,默认策略。 -
CallerRunsPolicy:由提交任务的线程执行。 -
DiscardPolicy:直接丢弃。 -
DiscardOldestPolicy:丢弃队列中最旧任务。
生产环境通常自定义拒绝策略,记录日志、报警或降级。
12. 为什么不建议使用 Executors 创建线程池?
因为部分工厂方法存在资源耗尽风险:
-
newFixedThreadPool:队列无界,可能 OOM。 -
newSingleThreadExecutor:队列无界,可能 OOM。 -
newCachedThreadPool:最大线程数极大,可能创建过多线程。 -
newScheduledThreadPool:队列无界。
建议手动创建 ThreadPoolExecutor,明确线程数、队列大小和拒绝策略。
13. ThreadLocal 原理和内存泄漏
每个线程内部有一个 ThreadLocalMap,key 是 ThreadLocal 弱引用,value 是存储的值。
内存泄漏原因:
-
key 是弱引用,可能被 GC。
-
value 是强引用,线程不结束时 value 仍存在。
使用建议:
try {
threadLocal.set(value);
// business code
} finally {
threadLocal.remove();
}
Spring / Spring Boot
1. Spring 核心思想
Spring 核心是 IOC 和 AOP。
-
IOC:控制反转,对象创建和依赖管理交给容器。
-
DI:依赖注入,是 IOC 的实现方式。
-
AOP:面向切面编程,将日志、事务、权限等横切逻辑从业务中抽离。
2. Bean 生命周期
典型生命周期:
-
实例化 Bean。
-
属性注入。
-
执行 Aware 接口回调。
-
BeanPostProcessor 前置处理。
-
初始化方法,如
@PostConstruct、afterPropertiesSet()、init-method。 -
BeanPostProcessor 后置处理。
-
Bean 可使用。
-
容器关闭时执行销毁方法。
3. Bean 作用域
-
singleton:单例,默认。 -
prototype:每次获取创建新实例。 -
request:每个 HTTP 请求一个实例。 -
session:每个 HTTP Session 一个实例。 -
application:ServletContext 级别。
4. Spring 如何解决循环依赖?
Spring 主要通过三级缓存解决单例 Bean 的 setter 循环依赖:
-
一级缓存:完整 Bean。
-
二级缓存:早期 Bean。
-
三级缓存:ObjectFactory,用于提前暴露对象或代理对象。
不能解决的情况:
-
构造器循环依赖。
-
prototype Bean 循环依赖。
5. AOP 原理
Spring AOP 基于动态代理:
-
如果目标类实现接口,默认使用 JDK 动态代理。
-
如果目标类没有实现接口,使用 CGLIB 代理。
常见术语:
-
切面 Aspect。
-
连接点 JoinPoint。
-
切点 Pointcut。
-
通知 Advice。
-
织入 Weaving。
常见应用:
-
声明式事务。
-
日志记录。
-
权限校验。
-
接口限流。
6. Spring 事务传播行为
常见传播行为:
-
REQUIRED:有事务则加入,没有则新建,默认。 -
REQUIRES_NEW:新建事务,挂起当前事务。 -
NESTED:嵌套事务。 -
SUPPORTS:有事务则加入,没有就非事务执行。 -
MANDATORY:必须在事务中执行。 -
NOT_SUPPORTED:非事务执行,挂起当前事务。 -
NEVER:必须非事务执行。
7. 事务失效常见原因
-
方法不是
public。 -
同类方法内部调用。
-
异常被捕获未抛出。
-
抛出的异常不是默认回滚异常。
-
数据库引擎不支持事务。
-
未被 Spring 管理。
-
多线程中事务上下文丢失。
默认情况下,Spring 事务只对运行时异常和 Error 回滚。受检异常需要配置:
@Transactional(rollbackFor = Exception.class)
8. Spring Boot 自动装配原理
Spring Boot 通过自动配置类,根据 classpath、配置文件和条件注解自动创建 Bean。
核心机制:
-
@SpringBootApplication-
@SpringBootConfiguration -
@EnableAutoConfiguration -
@ComponentScan
-
-
自动配置导入。
-
条件注解,如
@ConditionalOnClass、@ConditionalOnMissingBean。
9. Spring MVC 请求流程
-
请求进入
DispatcherServlet。 -
通过
HandlerMapping找到处理器。 -
通过
HandlerAdapter调用 Controller。 -
Controller 执行业务逻辑。
-
返回
ModelAndView或 JSON。 -
视图解析或消息转换。
-
响应客户端。
10. 常用注解
-
@Component:通用组件。 -
@Service:业务层。 -
@Repository:持久层。 -
@Controller/@RestController:控制层。 -
@Autowired:自动注入。 -
@Resource:按名称优先注入。 -
@Configuration:配置类。 -
@Bean:声明 Bean。 -
@Transactional:事务。
MyBatis
1. MyBatis 是什么?
MyBatis 是半自动 ORM 框架。开发者手写 SQL,MyBatis 负责参数映射、SQL 执行和结果映射。
优点:
-
SQL 灵活,适合复杂查询。
-
学习成本低。
-
与 Spring 集成成熟。
缺点:
-
需要维护 SQL。
-
数据库迁移成本相对更高。
2. #{} 和 ${} 区别
-
#{}:预编译参数,占位符,能防止 SQL 注入。 -
${}:字符串拼接,存在 SQL 注入风险。
排序字段、表名等不能使用 #{} 的场景,必须使用 ${} 时要做白名单校验。
3. MyBatis 一级缓存和二级缓存
-
一级缓存:SqlSession 级别,默认开启。
-
二级缓存:Mapper namespace 级别,需要手动开启。
实际项目中二级缓存较少使用,因为多表更新、分布式场景下容易产生一致性问题。
4. MyBatis 动态 SQL
常见标签:
-
<if> -
<where> -
<set> -
<foreach> -
<choose> -
<trim>
批量查询常用:
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
5. MyBatis 分页
常见方式:
-
SQL 中使用
limit。 -
使用 PageHelper 插件。
-
自定义拦截器。
大数据量分页不要使用过深的 offset,可使用游标分页或基于 ID 的分页。
MySQL
1. MySQL 存储引擎
常见存储引擎:
-
InnoDB:支持事务、行锁、外键、崩溃恢复,是默认引擎。
-
MyISAM:不支持事务,表锁,读性能较好,但现在较少使用。
2. 事务 ACID
-
Atomicity 原子性:事务要么全部成功,要么全部失败。
-
Consistency 一致性:事务前后数据满足约束。
-
Isolation 隔离性:并发事务之间互不干扰。
-
Durability 持久性:提交后的数据不会丢失。
3. 事务隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted | 可能 | 可能 | 可能 |
| Read Committed | 不可能 | 可能 | 可能 |
| Repeatable Read | 不可能 | 不可能 | InnoDB 通常可避免 |
| Serializable | 不可能 | 不可能 | 不可能 |
MySQL InnoDB 默认隔离级别是 Repeatable Read。
4. MVCC 原理
MVCC 是多版本并发控制,用于提高并发读写性能。
核心依赖:
-
隐藏字段:事务 ID、回滚指针。
-
undo log:保存历史版本。
-
ReadView:判断版本可见性。
快照读通过 MVCC 实现,当前读会加锁读取最新数据。
快照读示例:
select * from user where id = 1;
当前读示例:
select * from user where id = 1 for update; update user set name = 'A' where id = 1;
5. 索引数据结构
InnoDB 索引底层主要使用 B+ 树。
B+ 树适合数据库索引的原因:
-
树高度低,磁盘 IO 少。
-
非叶子节点只存 key,能放更多索引项。
-
叶子节点有序链表,适合范围查询。
6. 聚簇索引和非聚簇索引
-
聚簇索引:叶子节点存整行数据,InnoDB 主键索引就是聚簇索引。
-
二级索引:叶子节点存主键值,查询时可能需要回表。
回表:通过二级索引找到主键,再通过主键索引查询整行数据。
覆盖索引:查询字段都在索引中,无需回表。
7. 最左前缀原则
联合索引按照从左到右匹配。
例如索引:
idx(a, b, c)
可命中:
where a = 1 where a = 1 and b = 2 where a = 1 and b = 2 and c = 3
不能充分命中:
where b = 2 where c = 3
8. 索引失效常见场景
-
对索引列使用函数。
-
对索引列进行计算。
-
使用左模糊查询,如
like '%abc'。 -
类型隐式转换。
-
联合索引不满足最左前缀。
-
or两边不是都有索引。 -
低选择性字段不适合建索引。
9. 如何优化 SQL?
常见思路:
-
使用
explain分析执行计划。 -
避免
select *。 -
为高频查询条件建立合适索引。
-
使用覆盖索引减少回表。
-
避免索引失效。
-
控制返回数据量。
-
大表分页优化。
-
拆分复杂 SQL。
-
合理设计表结构。
10. explain 重点看什么?
重点字段:
-
type:访问类型,至少达到range,最好ref或const。 -
key:实际使用的索引。 -
rows:预估扫描行数。 -
Extra:额外信息,如Using index、Using filesort、Using temporary。
type 常见性能从好到差:
system > const > eq_ref > ref > range > index > ALL
11. MySQL 锁
按粒度:
-
表锁。
-
行锁。
-
间隙锁。
-
临键锁。
InnoDB 行锁基于索引实现。如果条件没有命中索引,可能退化为更大范围的锁。
12. 主从复制原理
MySQL 主从复制大致流程:
-
主库写入 binlog。
-
从库 IO 线程读取主库 binlog,写入 relay log。
-
从库 SQL 线程读取 relay log 并重放。
可能问题:
-
主从延迟。
-
数据一致性。
-
网络抖动。
-
大事务导致延迟。
Redis
1. Redis 为什么快?
主要原因:
-
基于内存。
-
单线程事件循环避免线程切换和锁竞争。
-
IO 多路复用。
-
高效数据结构。
-
C 语言实现,代码执行效率高。
Redis 6 引入多线程主要用于网络 IO,命令执行仍以单线程为主。
2. Redis 常见数据结构
-
String:缓存、计数器、分布式锁。
-
Hash:对象存储。
-
List:消息队列、列表。
-
Set:去重、共同好友。
-
ZSet:排行榜、延时队列。
-
Bitmap:签到、活跃统计。
-
HyperLogLog:UV 统计。
-
Stream:消息流。
3. Redis 持久化
-
RDB:快照持久化。
-
优点:文件紧凑,恢复快。
-
缺点:可能丢失最近一次快照后的数据。
-
-
AOF:追加日志。
-
优点:数据安全性更高。
-
缺点:文件更大,恢复较慢。
-
生产环境常结合 RDB + AOF。
4. Redis 过期删除策略
Redis 删除过期 key 的策略:
-
惰性删除:访问 key 时检查是否过期。
-
定期删除:周期性抽样删除过期 key。
如果内存仍不足,会触发内存淘汰策略。
5. Redis 内存淘汰策略
常见策略:
-
noeviction:不淘汰,写入报错。 -
allkeys-lru:所有 key 中淘汰最近最少使用。 -
volatile-lru:设置过期时间的 key 中淘汰 LRU。 -
allkeys-random:所有 key 随机淘汰。 -
volatile-random:过期 key 中随机淘汰。 -
volatile-ttl:优先淘汰即将过期的 key。 -
allkeys-lfu:所有 key 中淘汰低频使用。 -
volatile-lfu:过期 key 中淘汰低频使用。
6. 缓存穿透、击穿、雪崩
缓存穿透
查询不存在的数据,请求直接打到数据库。
解决:
-
缓存空值。
-
布隆过滤器。
-
参数校验。
缓存击穿
热点 key 过期,大量请求同时打到数据库。
解决:
-
互斥锁。
-
逻辑过期。
-
热点 key 永不过期。
-
提前异步刷新。
缓存雪崩
大量 key 同时过期或 Redis 故障。
解决:
-
过期时间加随机值。
-
多级缓存。
-
限流降级。
-
Redis 高可用。
7. Redis 分布式锁
基础命令:
SET lock_key unique_value NX PX 30000
释放锁要用 Lua 脚本保证“判断 value + 删除 key”的原子性。
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
需要注意:
-
锁超时时间。
-
业务执行时间超过锁时间。
-
锁误删。
-
主从切换导致锁丢失。
生产可使用 Redisson,支持看门狗自动续期。
8. Redis 主从、哨兵、集群
-
主从复制:读写分离,提高读能力。
-
哨兵 Sentinel:监控、故障转移、通知。
-
Cluster:数据分片,支持水平扩展。
Redis Cluster 使用 16384 个 hash slot 分配数据。
9. Redis 和 MySQL 数据一致性
常用方案:先更新数据库,再删除缓存。
为什么不是更新缓存?
-
并发下容易覆盖新值。
-
有些缓存值不是简单从数据库字段映射来的。
进一步优化:
-
删除缓存失败重试。
-
使用消息队列补偿。
-
监听 binlog 异步删除缓存。
-
设置合理过期时间兜底。
消息队列
1. 为什么使用消息队列?
主要作用:
-
异步处理:提升接口响应速度。
-
削峰填谷:缓冲高峰流量。
-
系统解耦:生产者和消费者解耦。
-
顺序处理:部分业务需要按顺序消费。
缺点:
-
系统复杂度增加。
-
可能出现消息丢失。
-
可能出现重复消费。
-
可能出现消息积压。
-
数据一致性更复杂。
2. 如何保证消息不丢失?
从三个阶段考虑:
-
生产者到 MQ:
-
开启 confirm 或事务机制。
-
失败重试。
-
-
MQ 自身:
-
持久化。
-
多副本。
-
-
MQ 到消费者:
-
手动 ack。
-
消费失败重试。
-
死信队列。
-
3. 如何解决重复消费?
消息队列通常只能尽量保证至少一次投递,因此消费者要保证幂等。
常见方案:
-
数据库唯一索引。
-
业务状态机。
-
Redis 去重。
-
消息表记录消费状态。
4. 如何保证消息顺序?
思路:同一业务 key 的消息进入同一个队列或分区,由同一个消费者顺序处理。
例如订单消息按 orderId 路由到同一分区。
注意:全局有序性能较差,实际更多是局部有序。
5. 消息积压怎么办?
排查方向:
-
消费者是否异常。
-
消费逻辑是否慢。
-
MQ 分区或队列数是否不足。
-
下游数据库是否瓶颈。
处理方式:
-
临时扩容消费者。
-
优化消费逻辑。
-
批量消费。
-
增加分区或队列。
-
先落库或转储后慢慢补偿。
计算机网络
1. OSI 七层模型和 TCP/IP 四层模型
OSI 七层:
应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
TCP/IP 四层:
应用层、传输层、网络层、网络接口层
常见协议:
-
HTTP / HTTPS:应用层。
-
TCP / UDP:传输层。
-
IP:网络层。
-
ARP:网络接口相关。
2. TCP 和 UDP 区别
| 对比项 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接 | 无连接 |
| 可靠性 | 可靠 | 不可靠 |
| 顺序性 | 保证顺序 | 不保证 |
| 速度 | 较慢 | 较快 |
| 场景 | Web、文件传输 | 直播、游戏、DNS |
3. TCP 三次握手
流程:
-
客户端发送 SYN。
-
服务端返回 SYN + ACK。
-
客户端返回 ACK。
为什么不是两次?
为了确认双方收发能力都正常,避免历史连接请求导致错误连接。
4. TCP 四次挥手
流程:
-
主动关闭方发送 FIN。
-
被动关闭方返回 ACK。
-
被动关闭方处理完数据后发送 FIN。
-
主动关闭方返回 ACK。
为什么需要四次?
因为 TCP 是全双工,双方发送通道需要分别关闭。
5. HTTP 和 HTTPS 区别
-
HTTP 明文传输,默认 80 端口。
-
HTTPS 基于 HTTP + TLS,默认 443 端口。
HTTPS 提供:
-
加密传输。
-
身份认证。
-
完整性校验。
6. HTTP 状态码
-
200:成功。 -
301:永久重定向。 -
302:临时重定向。 -
400:请求参数错误。 -
401:未认证。 -
403:无权限。 -
404:资源不存在。 -
500:服务端异常。 -
502:网关错误。 -
503:服务不可用。 -
504:网关超时。
7. GET 和 POST 区别
常见区别:
-
GET 通常用于查询,POST 通常用于提交。
-
GET 参数一般在 URL 中,POST 参数通常在请求体。
-
GET 更容易被缓存。
-
GET 幂等,POST 通常不幂等。
注意:安全性不取决于 GET/POST,而取决于是否使用 HTTPS、鉴权和参数保护。
8. 浏览器输入 URL 后发生了什么?
简化流程:
-
URL 解析。
-
DNS 解析域名。
-
建立 TCP 连接。
-
HTTPS 场景进行 TLS 握手。
-
发送 HTTP 请求。
-
服务端处理请求。
-
返回响应。
-
浏览器解析渲染页面。
-
连接复用或关闭。
Linux 与排查
1. 常用命令
top # 查看系统资源 ps -ef | grep java # 查看 Java 进程 netstat -tulnp # 查看端口 ss -tulnp # 查看端口 df -h # 查看磁盘 du -sh * # 查看目录大小 free -m # 查看内存 tail -f app.log # 实时日志 grep "ERROR" app.log
2. CPU 飙高如何排查?
步骤:
-
top找到 CPU 高的 Java 进程 PID。 -
top -Hp PID找到高 CPU 线程 TID。 -
将 TID 转 16 进制。
-
jstack PID查看线程栈。 -
根据线程栈定位代码。
printf "%x\n" 12345 jstack <pid> | grep -A 30 <nid>
3. 内存溢出如何排查?
常见步骤:
-
查看错误日志,确认 OOM 类型。
-
导出堆 dump。
-
使用 MAT、VisualVM、JProfiler 分析大对象。
-
找 GC Roots 引用链。
-
修复内存泄漏或调整参数。
建议启动参数:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump
4. 接口变慢如何排查?
排查路径:
-
看应用日志耗时。
-
看数据库慢 SQL。
-
看 Redis、MQ、第三方接口耗时。
-
看线程池是否满。
-
看 GC 是否频繁。
-
看机器 CPU、内存、磁盘 IO、网络。
-
看链路追踪。
分布式与微服务
1. 分布式系统常见问题
-
服务调用失败。
-
网络延迟和超时。
-
数据一致性。
-
分布式事务。
-
服务雪崩。
-
配置管理。
-
链路追踪。
-
日志聚合。
2. CAP 理论
CAP:
-
Consistency:一致性。
-
Availability:可用性。
-
Partition tolerance:分区容错性。
分布式系统中网络分区不可避免,所以通常在 CP 和 AP 之间权衡。
3. BASE 理论
BASE 是对强一致性的弱化:
-
Basically Available:基本可用。
-
Soft State:软状态。
-
Eventually Consistent:最终一致性。
很多互联网系统选择最终一致性。
4. 分布式事务解决方案
常见方案:
-
2PC:强一致,但性能差、阻塞风险。
-
TCC:Try、Confirm、Cancel,业务侵入强。
-
本地消息表:最终一致,可靠性高。
-
MQ 事务消息:适合异步最终一致。
-
Saga:长事务拆分,逐步补偿。
5. 分布式 ID
常见方案:
-
数据库自增 ID。
-
UUID。
-
Redis 自增。
-
Snowflake 雪花算法。
-
号段模式。
Snowflake 通常包含:
-
时间戳。
-
机器 ID。
-
序列号。
优点是趋势递增、性能高、适合分布式。
6. 限流算法
-
固定窗口:简单,但边界可能突刺。
-
滑动窗口:更平滑。
-
漏桶:恒定速率处理请求。
-
令牌桶:允许一定突发流量。
常见限流位置:
-
网关。
-
Nginx。
-
应用接口。
-
Redis + Lua。
7. 熔断、降级、隔离
-
熔断:服务异常比例过高时,暂时停止调用。
-
降级:返回兜底数据或关闭非核心功能。
-
隔离:线程池、信号量、资源隔离,避免故障扩散。
8. 服务注册与发现
常见组件:
-
Nacos。
-
Eureka。
-
Consul。
-
ZooKeeper。
流程:
-
服务启动后注册到注册中心。
-
消费者从注册中心获取服务列表。
-
客户端负载均衡选择实例。
-
通过心跳维持服务状态。
9. 网关作用
网关常见职责:
-
路由转发。
-
鉴权。
-
限流。
-
灰度发布。
-
日志记录。
-
跨域处理。
-
协议转换。
项目面试高频追问
1. 介绍一下你的项目
建议回答结构:
-
项目背景:解决什么问题。
-
技术栈:Spring Boot、MyBatis、MySQL、Redis、MQ 等。
-
个人职责:你负责哪些模块。
-
核心难点:性能、一致性、并发、可用性。
-
结果数据:QPS、响应时间、数据量、稳定性提升。
2. 登录认证怎么做?
常见方案:
-
Session + Cookie。
-
JWT。
-
OAuth2。
-
单点登录 SSO。
JWT 流程:
-
用户登录成功后生成 token。
-
token 返回客户端。
-
后续请求携带 token。
-
服务端校验签名和过期时间。
-
从 token 或 Redis 中获取用户信息。
注意问题:
-
token 过期。
-
token 续期。
-
退出登录。
-
权限变更后 token 失效。
-
防止 token 被盗用。
3. 权限系统怎么设计?
常见模型:RBAC。
用户 -> 角色 -> 权限
常见表:
-
用户表。
-
角色表。
-
权限表。
-
用户角色关联表。
-
角色权限关联表。
接口权限可通过拦截器、过滤器、AOP 或 Spring Security 实现。
4. 秒杀系统怎么设计?
核心问题:
-
高并发。
-
防超卖。
-
防刷。
-
异步削峰。
-
数据一致性。
常见方案:
-
页面静态化。
-
CDN。
-
接口限流。
-
Redis 预扣库存。
-
Lua 保证原子性。
-
MQ 异步下单。
-
数据库乐观锁兜底。
-
幂等控制。
5. 如何防止重复提交?
方案:
-
前端按钮置灰。
-
token 防重复提交。
-
数据库唯一索引。
-
Redis
setnx。 -
幂等号。
-
状态机控制。
6. 如何设计幂等接口?
常见思路:
-
请求唯一 ID。
-
数据库唯一约束。
-
Redis 去重。
-
乐观锁版本号。
-
业务状态机。
例如支付回调:
-
先查询订单状态。
-
已支付则直接返回成功。
-
未支付则更新状态。
-
更新条件中带原状态。
update order_info set status = 'PAID' where order_no = ? and status = 'UNPAID';
7. 如何做接口限流?
可选方案:
-
本地限流:Guava RateLimiter、Sentinel。
-
分布式限流:Redis + Lua。
-
网关限流:Spring Cloud Gateway、Nginx。
需要考虑:
-
限流维度:用户、IP、接口、租户。
-
超限响应。
-
白名单。
-
监控告警。
8. 如何保证接口安全?
常见措施:
-
HTTPS。
-
登录认证。
-
权限校验。
-
参数校验。
-
签名机制。
-
时间戳 + nonce 防重放。
-
SQL 注入防护。
-
XSS / CSRF 防护。
-
敏感信息脱敏。
手写题与场景题
1. 手写单例模式
静态内部类方式:
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
优点:懒加载、线程安全、实现简单。
2. 手写双重检查锁单例
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile 用于防止指令重排序,避免其他线程拿到未初始化完成的对象。
3. 手写 LRU 缓存
import java.util.LinkedHashMap;
import java.util.Map;
public class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LruCache(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
4. 两个线程交替打印
public class AlternatePrint {
private static final Object LOCK = new Object();
private static int num = 1;
public static void main(String[] args) {
Runnable task = () -> {
while (true) {
synchronized (LOCK) {
if (num > 100) {
LOCK.notifyAll();
break;
}
System.out.println(Thread.currentThread().getName() + ": " + num++);
LOCK.notifyAll();
try {
LOCK.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
};
new Thread(task, "A").start();
new Thread(task, "B").start();
}
}
5. 生产者消费者模型
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ProducerConsumer {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
Thread producer = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
queue.put(i);
System.out.println("produce: " + i);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
Thread consumer = new Thread(() -> {
while (true) {
try {
Integer value = queue.take();
System.out.println("consume: " + value);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
producer.start();
consumer.start();
}
}
6. 场景题:如何设计排行榜?
可以使用 Redis ZSet。
-
用户 ID 作为 member。
-
分数作为 score。
-
ZADD更新分数。 -
ZREVRANGE查询榜单。 -
ZREVRANK查询排名。
需要考虑:
-
分数相同的排序规则。
-
榜单周期:日榜、周榜、总榜。
-
数据持久化到 MySQL。
-
热点榜单缓存。
7. 场景题:如何设计延时任务?
常见方案:
-
Redis ZSet:score 存执行时间。
-
MQ 延迟队列。
-
时间轮。
-
数据库定时扫描。
Redis ZSet 方案:
-
任务写入 ZSet,score 是执行时间戳。
-
定时扫描到期任务。
-
抢占成功后执行任务。
-
执行失败进入重试或死信。
需要考虑任务重复执行、宕机恢复和执行幂等。
高频速记
Java
-
String不可变,拼接多用StringBuilder。 -
equals()相等必须保证hashCode()相等。 -
反射常用于框架,缺点是性能和封装性。
集合
-
HashMap是数组 + 链表 + 红黑树。 -
ConcurrentHashMapJDK 8 是 CAS + synchronized。 -
ArrayList查询快,LinkedList插入删除灵活但内存开销大。
JVM
-
堆存对象,栈存方法调用。
-
GC Roots 可达性分析判断对象是否回收。
-
G1 是常见服务端垃圾收集器。
-
双亲委派保护核心类,避免重复加载。
并发
-
volatile保证可见性和有序性,不保证复合操作原子性。 -
CAS 是乐观锁,但有 ABA 问题。
-
线程池核心参数必须会背。
-
ThreadLocal用完要remove()。
Spring
-
IOC 管对象,AOP 管切面。
-
Spring 三级缓存解决单例 setter 循环依赖。
-
事务失效重点看代理、异常、方法可见性、同类调用。
MySQL
-
InnoDB 支持事务和行锁。
-
B+ 树适合范围查询和减少磁盘 IO。
-
联合索引遵循最左前缀。
-
MVCC 依赖 undo log 和 ReadView。
Redis
-
Redis 快的原因:内存、单线程、IO 多路复用、高效结构。
-
缓存穿透、击穿、雪崩要会区分。
-
分布式锁用
SET NX PX+ Lua 解锁。
分布式
-
MQ 用于异步、削峰、解耦。
-
重复消费靠幂等。
-
分布式事务通常追求最终一致。
-
限流、熔断、降级、隔离是高可用常见手段。
面试回答模板
技术点回答模板
这个问题我会从四个方面回答: 第一,它是什么; 第二,为什么要用它; 第三,它的底层原理或核心流程; 第四,实际项目中有哪些注意点或坑。
项目亮点回答模板
这个模块我主要负责 XXX。 当时遇到的问题是 XXX。 我采用了 XXX 方案,核心思路是 XXX。 落地过程中重点处理了 XXX,比如并发、一致性、幂等、性能。 最终效果是 XXX,比如接口耗时降低、稳定性提升、支持更高并发。
场景题回答模板
我会先明确业务目标和约束,然后从数据模型、核心流程、并发控制、 异常补偿、监控告警和扩展性几个方面设计。 如果是高并发场景,还会考虑缓存、限流、异步削峰和降级方案。
复习建议
-
第一轮:通读所有模块,建立知识地图。
-
第二轮:重点背 Java 集合、JVM、并发、Spring、MySQL、Redis。
-
第三轮:结合自己的项目,把每个技术点落到项目场景中。
-
第四轮:模拟面试,用“是什么、为什么、怎么做、有什么坑”回答。
-
面试前一天:重点复习高频速记、项目追问和手写题。
更多推荐
所有评论(0)