ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

2026年Java面试26个核心考点:从原理到实战的完整追问指南

2026年Java面试26个核心考点:从原理到实战的完整追问指南

最近后台收到一堆同学的面试复盘,印象最深的一条是:面某中厂后端,三轮面试没有一轮直接问“HashMap原理背一遍”,而是从“线上偶发死锁怎么排查”聊起,顺藤摸瓜把线程状态、锁升级、字节码甚至操作系统调度全串了一遍。这就是2026年Java面试的常态——考点还是那26个核心考点,但问法已经从“填空题”变成了“案件重演”。

这篇文章把2026年3月前后最常出现的Java面试核心考点做了系统整理,每个考点都配了代码示例或排查路径,适合正在准备中级、高级Java岗位面试的同学,也适合想系统自查技术体系的在职开发。文章不按教科书目录排,而是按面试官实际追问的逻辑排——你先看到的是“为什么会这么问”,然后才是“该怎么答”。

1. 2026年Java面试风向:八股文不再是唯一标尺

1.1 面试形式变了:从背答案到造场景

过去大家理解的面试题是“ConcurrentHashMap说一下原理”“线程池参数有哪些”,标准答案背熟就能过。现在的主流问法是给一个场景,让你自己发现问题、拆解问题、给出方案。

比如某同学复盘时提到,面试官问的不是“Redis为什么快”,而是“你们缓存命中率突然从95%掉到70%,你怎么定位”。这道题表面上考缓存,实际把过期策略、键设计、大Key、热Key、持久化阻塞、网络IO全扯进来了。你需要现场画一条排查链路,而不是背一段结论。

所以2026年的备考方式,必须从“记答案”改成“建模型”。每个考点背后都要明白至少两层:第一层是什么(定义和原理),第二层是为什么(设计动机和适用边界)。能答出第二层,面试官才会认为你有实战判断力,而不仅仅是个移动题库。

1.2 26个考点的分布逻辑与复习优先级

我按面试中出现频率和追问深度,把这26个考点分成五个大模块:JVM与性能、并发编程、集合与语言特性、数据库与缓存、Spring与微服务。这个分布不是随意凑的,它对应的是后端岗位日常工作中最常出问题的五个区域:

  • JVM和并发:线上故障排查的重灾区,OOM、死锁、CPU飙高都在这两个模块里。
  • 集合和语言特性:源码级基础,面试官用来快速判断代码功底。
  • 数据库和缓存:绝大多数业务系统的性能瓶颈所在,也是场景题最爱出题的地方。
  • Spring与微服务:项目经验的必答区,尤其事务和分布式一致性。

复习优先级上,我建议把并发和数据库缓存放在最前面,因为这两个模块能同时覆盖“原理题”和“场景题”。集合和JVM可以穿插着看,语言特性(比如Record、Sealed Class)属于性价比极高的考点,背一下就能拿分,但很多人容易忽略。

2. JVM与并发:高频题里的“体力活”与“硬技术”

2.1 JVM内存模型与“对象一定在堆上吗”的反直觉答案

JVM内存分区是送分题,但2026年的面试官不会满足于“堆、栈、方法区”六个字。真正的分水岭问题有两个:对象一定分配在堆上吗?什么时候会在栈上分配?

答案是否定的。当对象未发生逃逸时,JVM通过逃逸分析确认它不会被外部方法访问,可能将对象拆散并分配到栈上或寄存器中,这就是栈上分配。典型场景是循环内部创建的局部对象,比如:

public long sum(long[] arr) { long total = 0; for (long v : arr) { Point p = new Point(v, v); // 未逃逸,可能栈上分配 total += p.x + p.y; } return total; }

这段代码里的Point对象每次循环都会创建,如果都进堆,GC压力会非常大。逃逸分析开启后(默认开启),它可能被分配到栈上,方法结束即销毁,完全不用GC介入。

搭配记忆的点还有两个:TLAB(线程本地分配缓冲)解决的是“多个线程在堆上并发分配时的竞争问题”,每个线程在Eden区划一块私有空间;指针压缩则是在堆内存小于32GB时,通过压缩普通对象指针来减少内存占用。这三件事连起来,才算把“对象分配”这个环节讲透。

2.2 垃圾回收器选型:G1 和 ZGC 的取舍,别只看暂停时间

G1已经是老生常谈,但2026年还在大量线上系统服役。它的核心思路是把堆分成若干大小不一的Region,通过维护一个优先级列表,优先回收价值最大的Region,目标是在可控暂停时间内完成尽可能多的回收。

ZGC的特点是几乎不暂停应用线程,依靠染色指针和读屏障实现并发标记、并发转移。它的停顿时间基本稳定在1毫秒以内,而且不随堆大小增长。但ZGC不是银弹,它更吃内存和CPU,适合大堆、低延迟场景。

我整理了一份选型对比,面试时可以直接用:

对比维度G1ZGC
设计目标可预测的暂停时间(默认200ms)极低停顿(亚毫秒级)
实现基础Region分代回收染色指针、读屏障、并发转移
适用堆大小中等大堆(几GB到几十GB)超大堆(几十GB到数百GB)
主要代价需调优Region和暂停目标额外内存开销与CPU占用
常见参数-XX:MaxGCPauseMillis=200-XX:+UseZGC

追问环节常问“线上什么时候该换ZGC”。我的建议答案是:当业务对延迟极其敏感、堆内存已经无法通过优化代码降低、G1的暂停时间已经影响可用性时,才考虑换。换之前先做压测对比,观察最坏停顿,而不是平均停顿。

2.3 锁的升级过程与 ReentrantLock 的底层逻辑

synchronized和ReentrantLock的区别是经典题,但高分回答要能讲出锁升级的完整链路:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。偏向锁假设同一线程反复获取锁,用一次CAS记录线程ID;一旦有竞争,升级为轻量级锁,通过自旋在用户态争取;自旋超过阈值或竞争激烈,才升级为重量级锁,进入操作系统互斥量,线程真正阻塞。

这里有个容易答错的坑:偏向锁在JDK 15中默认被禁用,JDK 17之后已经彻底移除。很多八股文还在讲偏向锁是默认开启的,2026年面试这么说会露馅。新版本里锁启动直接进入轻量级锁阶段,自旋选项也不再需要手动配置。

ReentrantLock相对synchronized的优势,核心是可中断、可超时、可多条件绑定。写一个标准模板:

Lock lock = new ReentrantLock(); Condition notEmpty = lock.newCondition(); // 加锁 lock.lock(); try { // 临界区代码 // condition.await() / signal() } finally { lock.unlock(); }

实际开发中最容易犯的错是try块里加锁、finally块里忘记解锁,或者把lock()放进try里导致加锁失败也走finally解锁,结果抛出IllegalMonitorStateException。模板的正确写法是lock()必须在try之前,unlock()必须放在finally第一行。

2.4 线程池参数设计:永远不要只用默认值

线程池参数面试问得极多,但很多人只背参数名,答不上来具体怎么定。ThreadPoolExecutor七个参数里,最核心的是核心线程数、最大线程数、阻塞队列和拒绝策略。

核心线程数不是算出来的,而是根据任务类型区分的。CPU密集型任务,核心线程数≈CPU核数+1;IO密集型任务,核心线程数可以放宽到CPU核数×2甚至更多,因为大部分时间线程在等待IO,不占用CPU。更合理的做法是结合压测结果调整,而不是套公式。

拒绝策略的选择也很有讲究。AbortPolicy直接抛异常,适合不允许丢弃任务的场景;CallerRunsPolicy让提交者线程自己执行任务,适合需要降速保底、又不希望任务丢失的场景;DiscardOldestPolicy丢弃最旧任务,适合实时性要求高的缓存场景。

ThreadPoolExecutor executor = new ThreadPoolExecutor( 8, // 核心线程数 16, // 最大线程数 60, TimeUnit.SECONDS, // 空闲线程存活时间 new ArrayBlockingQueue<>(100), // 有界队列 new ThreadFactoryBuilder().setNameFormat("business-pool-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy() );

还有个高频细节:队列用有界还是无界。无界队列会让最大线程数参数失去意义,任务无限堆积,最终内存溢出;有界队列配合拒绝策略,才能让线程池在流量洪峰时表现为“排队+限流”,而不是“吞掉所有任务直到崩溃”。

2.5 虚拟线程与结构化并发:Java 21后的必问题

虚拟线程是2026年面试绕不开的新考点。它的核心价值是让Java以极低成本创建海量线程,适合IO密集型任务。虚拟线程由JVM调度,挂载在少数平台线程上执行,遇到IO阻塞时自动卸载,让出载体线程,等待IO完成后重新挂载。

写一个虚拟线程请求外部服务的典型示例:

// 每个请求一个虚拟线程,成本极低 try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { Future<Result> r1 = executor.submit(() -> callServiceA()); Future<Result> r2 = executor.submit(() -> callServiceB()); Future<Result> r3 = executor.submit(() -> callServiceC()); Result result = new Result( r1.get(), r2.get(), r3.get() ); } // try-with-resources 自动等待所有任务完成并关闭

结构化并发(StructuredTaskScope)则是把并发任务的创建、执行、取消纳入同一个代码块作用域,任务要么全部成功,要么统一失败处理,避免线程泄漏和孤立任务。这是Java 21之后的并发编程新范式,面试官如果问“虚拟线程会不会让线程池失效”,可以回答:线程池依然适用于CPU密集型和需要限流的场景,虚拟线程真正改变的是IO密集型任务的资源模型。

3. 集合与语言特性:从源码看面试官的追问轨迹

3.1 HashMap 的扩容、红黑树与容量陷阱

HashMap考点密集,最容易丢分的是三个细节。第一个是容量始终是2的幂,目的是让hash散列后的下标计算用位运算(n - 1) & hash替代取模,速度更快。第二个是链表转红黑树的阈值是8,但触发条件有两个:链表长度达到8,且数组长度不小于64。如果数组长度没到64,会优先扩容而不是树化。第三个是红黑树退化回链表的阈值是6,预留了1的差值避免频繁转换。

还有一个容易被追问的并发问题:JDK 7的HashMap在多线程扩容时会形成环形链表导致死循环,JDK 8改成尾插法解决了死循环,但并发put仍然会丢数据。所以面试题标准答案是“HashMap线程不安全,并发场景用ConcurrentHashMap”,但你要能解释为什么换了尾插法还是不安全,因为两个线程同时扩容时,彼此的引用拷贝依然可能覆盖。

代码层面可以准备一个手写hash函数的变体:

static int hash(Object key) { int h; // 高16位与低16位异或,让高位参与下标计算,降低碰撞概率 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

追问“为什么是异或而不是直接取低位”时,回答思路是:如果两个hashCode低16位相同、高16位不同,不扰动就会发生碰撞;异或把高位信息混入低位,让下标分布更均匀。

3.2 ConcurrentHashMap 的锁粒度演进

JDK 7的ConcurrentHashMap采用分段锁,默认16个Segment,每个Segment继承ReentrantLock,锁粒度是Segment级别。JDK 8彻底抛弃分段锁,改为CAS + synchronized锁单个数组节点,锁粒度细化到桶。初始化时不再预先分配Segment数组,而是惰性创建Node数组。

核心源码逻辑可以概括为三件事:空桶通过CAS无锁插入;非空桶通过synchronized锁住头节点;扩容时多个线程协作迁移数据,而不是单线程rehash。synchronized在JDK 8之后做了锁升级优化,所以锁单个节点的成本远低于想象,这也让ConcurrentHashMap在高并发读写下表现出色。

面试官常追问的是size()方法。JDK 8里没有全局锁,而是维护一个baseCount和CounterCell数组,并发加元素时通过CAS分散计数,最后累加。所以size()返回的是一个近似值,在并发环境下并不保证绝对准确。

3.3 fail-fast 与 fail-safe 的真实触发条件

集合遍历时出现ConcurrentModificationException,是fail-fast机制在起作用。它的实现方式是ArrayList、HashMap等集合内部维护一个modCount字段,每次结构性修改(add、remove、clear)都会modCount++。迭代器在创建时记录expectedModCount,每次next()都会检查两者是否相等,不相等就立刻抛异常。

这里有个容易忽略的细节:普通set操作不会增加modCount,所以并发修改元素值不会触发异常,只有结构性修改才会。CopyOnWriteArrayList是典型的fail-safe集合,写操作拷贝新数组,迭代器遍历的是原数组快照,因此不会抛异常,但遍历期间看不到新写入的数据,而且每次写都是数组拷贝,写频繁场景性能很差。

面试回答这类问题时,最好把两个机制放在同一维度对比:fail-fast是“检测到并发修改就快速失败”,fail-safe是“读旧快照保证遍历稳定”,后者的代价是内存和写性能。

3.4 Record、Sealed Class 与模式匹配的实用场景

语言特性题属于性价比最高的考点,因为代码简单、答案明确,而且能直接体现你是否关注新版本。Record是不可变数据载体,自动生成构造器、equals、hashCode、toString,适合做DTO和VO。

public record OrderCreatedEvent( Long orderId, Long userId, BigDecimal amount ) { // 压缩构造器:在构造时校验参数 public OrderCreatedEvent { if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("amount must be positive"); } } }

Sealed Class解决的是继承范围失控的问题:一个类允许哪些子类继承,由程序员显式声明。

public sealed interface PayResult permits SuccessResult, FailResult, RetryResult { } record SuccessResult(String trxId, BigDecimal paidAmount) implements PayResult {} record FailResult(String code, String reason) implements PayResult {} record RetryResult(int retryTimes) implements PayResult {}

配合Java 17+的switch模式匹配,可以写出非常清晰的逻辑:

String describe(PayResult result) { return switch (result) { case SuccessResult s -> "支付成功,金额:" + s.paidAmount(); case FailResult f -> "支付失败:" + f.reason(); case RetryResult r -> "需要重试,当前次数:" + r.retryTimes(); }; }

追问陷阱通常是“Record和Lombok的@Data有什么区别”。要点是Record是语言级不可变约束,所有字段final,且继承自java.lang.Record,不能扩展父类;Lombok本质是编译期注解处理器,生成的类仍然是普通类。Record更适合值对象,Lombok更适合需要灵活可变对象的场景。

4. 数据库与缓存:一条 SQL 能问出三轮“为什么”

4.1 索引结构、最左前缀与回表

索引题最核心的是理解B+树为什么是默认结构。B+树内部节点不存数据,只存索引键,因此单节点能容纳更多键,树更矮,IO次数更少;叶子节点用双向链表串联,范围查询只需顺序遍历叶子。

聚簇索引的叶子节点存储整行数据,二级索引的叶子节点存储主键值。所以通过二级索引查询但需要非索引列时,要利用主键回表。覆盖索引则是索引本身包含所有需要查询的列,不用回表。

设计索引时的核心规则是最左前缀原则:联合索引(a, b, c)能命中(a)、(a,b)、(a,b,c),但(b,c)不知道。因为B+树先按a排序,再按b排序,最后按c排序,跳过了a就无法利用索引的有序性。

CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, status TINYINT NOT NULL, create_time DATETIME NOT NULL, amount DECIMAL(12,2), KEY idx_user_status_time (user_id, status, create_time) ); -- 命中索引:(user_id) -- 命中索引:(user_id, status) -- 命中索引:(user_id, status, create_time) -- 不命中最左前缀:(status, create_time)

最容易踩的坑是在索引列上做函数运算,比如WHERE DATE(create_time) = '2026-01-01',这样索引列被函数包装,B+树无法按原键值二分定位,索引失效。正确写法是范围条件WHERE create_time >= '2026-01-01' AND create_time < '2026-01-02'。

4.2 事务隔离级别与 MVCC 的实现闭环

隔离级别这个考点,抽背定义只是基础,高分答案要把“问题-级别-MVCC实现”一条线讲通。

隔离级别解决的并发问题仍存在的问题
读未提交无脏写、脏读、不可重复读、幻读
读已提交脏读不可重复读、幻读
可重复读脏读、不可重复读幻读
串行化全部并发度极低

MVCC(多版本并发控制)是读已提交和可重复读的实现基石。每行数据有隐藏列:事务ID和回滚指针;undo log维护版本链;每个事务有一个ReadView,记录活跃事务列表。

可重复读和读已提交的区别,核心在于ReadView的生成时机。读已提交是每次SELECT都生成新ReadView,所以能看到别的事务新提交的数据;可重复读是事务内第一次SELECT生成ReadView后固定复用,后续SELECT永远只看到这个快照。这也是为什么可重复读下幻读不“完全”出现,但如果是当前读(SELECT ... FOR UPDATE),还是可能通过间隙锁才保证不出现幻读。

面试官如果追问“可重复读为什么能解决不可重复读”,最直接的回答是:它本质上不是“禁止别人修改”,而是“修改后的新版本对当前事务不可见”,这就是快照隔离的含义。

4.3 慢 SQL 排查:EXPLAIN 里的几个关键字段

场景题“线上有一条SQL突然变慢”是2026年的高频考点。排查路径应该是:先确认慢日志开关和阈值,再拿到具体SQL,然后EXPLAIN查看执行计划,最后确定是索引失效还是扫描行数爆炸。

EXPLAIN需要重点看的字段有四个:

字段关注点危险信号
type访问类型出现ALL说明全表扫描,至少要达到range或ref
key实际使用索引NULL表示没走索引
rows预估扫描行数远大于实际数据量说明统计信息或SQL写法有问题
Extra额外信息Using filesort、Using temporary、Using index condition需注意

典型案例如下:

EXPLAIN SELECT order_id, user_id, amount FROM orders WHERE user_id = 10086 ORDER BY create_time;

如果type是index且Extra是Using filesort,说明优化器选了二级索引但不满足排序,需要额外排序。解决思路通常是加索引(user_id, create_time),让索引顺序直接覆盖排序需求,避免filesort。

4.4 Redis 持久化与缓存一致性怎么答

Redis持久化考的是RDB与AOF的取舍。RDB是内存快照,恢复快、文件紧凑,但两次快照之间的数据可能丢失;AOF是操作日志追加,丢数据少,但文件体积大、重启恢复慢。生产环境常用混合持久化:AOF重写后生成RDB快照作为基础,再追加增量AOF日志,兼顾恢复速度和数据安全。

缓存一致性更常以场景题出现:更新数据库后缓存怎么删、先删缓存还是先写库。目前实践中最稳妥的主流方案是Cache Aside:读的时候先读缓存,未命中则读库并回填;写的时候先更新数据库,再删除缓存。

脏数据最容易出现在“先删缓存再写库”的流程里:线程A删缓存,线程B立刻读库并把旧值回填缓存,随后线程A才更新数据库,于是缓存永远停留在旧值。相比之下“先更新数据库再删缓存”,并发窗口小很多,配合延迟双删(先删缓存→写库→休眠一小段→再删缓存)能进一步兜底。

面试时不要只背方案,要说出权衡:完全没有一致性问题是不可能的,除非串行化;实际系统能做的是把脏窗口压缩到极致,再配合过期时间兜底,让数据最终一致。

4.5 分布式锁:从 setnx 到 RedLock 的演进

分布式锁是最典型的“从简单方案推到复杂方案”考点。最早的做法是用SETNX,但必须带过期时间,否则持锁进程宕机锁永远不释放。正确命令是:

SET lock_key unique_value NX EX 30

这里unique_value必须唯一,用于释放锁时校验“锁是我加的”,防止误删别人加的锁。释放操作要保证原子性,写成Lua脚本:

if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

单机Redis锁存在主从切换时锁丢失的风险,于是有了RedLock的思想:向多个独立节点依次加锁,超过半数成功才算加锁成功。但RedLock本身也有争议,很多人认为它复杂且不一定比单机加锁更安全。面试时可以说出这个权衡:多数业务场景对绝对一致要求没那么高,单机Redis锁配合看门狗续期已经够用;如果真需要高可用,分布式协同组件提供的临时顺序节点方案可能更可靠。

5. Spring与微服务:项目经验的“问薄了”现场

5.1 Bean 生命周期与三级缓存循环依赖

Spring Bean生命周期是一条完整的链:实例化→属性填充→初始化前→初始化→初始化后(AOP代理生成)→使用→销毁。最常被追问的是循环依赖怎么解决。

Spring解决循环依赖靠的是三级缓存:

// 第一级:已完成完整生命周期的单例Bean Map<String, Object> singletonObjects; // 第二级:提前暴露的早期Bean引用(尚未完成属性填充和初始化) Map<String, Object> earlySingletonObjects; // 第三级:ObjectFactory,用于生成早期Bean引用的工厂 Map<String, ObjectFactory<?>> singletonFactories;

流程大致是:实例化A后,A先把自己包装成ObjectFactory放入三级缓存,再填充属性时发现需要B,于是去创建B;B填充属性时发现需要A,此时从三级缓存拿到A的早期引用,B完成创建;A从B的创建过程中拿到B的引用并继续完成自己的属性填充和初始化。

面试官常设的坑有两个。第一个是“构造器注入的循环依赖为什么解决不了”,因为Bean实例化必须调用构造器,A还没实例化完就无法放入三级缓存,B拿不到A的引用。第二个是“三级缓存为什么是ObjectFactory而不是直接放对象”,因为Spring可能在最后一步生成AOP代理,如果早期直接放原始对象,后面无法替换成代理对象;用ObjectFactory可以延迟到真正需要引用时再决定暴露原始对象还是代理对象。

5.2 事务传播行为:五个默认值里藏着几个坑

事务传播行为是Spring面试的高频细节题。传播行为定义了“当前方法的事务如何与已有事务交互”。最常用的是REQUIRED、REQUIRES_NEW、NESTED。

传播行为行为描述典型使用场景
REQUIRED有事务则加入,无事务则新建默认值,大多数业务方法
REQUIRES_NEW挂起当前事务,新开独立事务审计日志、异步通知,失败不影响主流程
NESTED嵌套事务,基于保存点回滚批量处理中允许部分失败
SUPPORTS有事务则加入,无事务则不开启查询方法
NOT_SUPPORTED挂起当前事务,以非事务方式执行大数据量导出,避免长事务

实际开发最容易踩的坑是事务自调用失效:同一个类里方法A调用方法B,B上的@Transactional不生效。因为代理机制只在外部调用时生效,内部this.method()调用走的是原始对象,不是代理对象。解决方式是拆分到不同Bean,或者通过AopContext.currentProxy()。

另一个高频坑是异常被吞导致事务不滚。@Transactional默认只对RuntimeException和Error回滚,检查异常不会回滚。如果业务方法捕获异常后不抛出,事务必然正常提交,数据就半途落库了。

@Transactional(rollbackFor = Exception.class) public void createOrder(OrderDTO dto) { try { orderMapper.insert(dto); paymentService.deduct(dto.getUserId(), dto.getAmount()); } catch (Exception e) { // 这里重新抛RuntimeException,事务才会回滚 throw new BizException("创建订单失败", e); } }

5.3 分布式事务:2PC、TCC、Saga 怎么选

分布式事务是微服务架构的高阶考点。面试官通常不会让你把协议全程推演一遍,而是给一个业务场景,让你选方案并说理由。

2PC(两阶段提交)是最直观的方案,分为准备阶段和提交阶段,协调者通知所有参与者准备,全部成功后统一提交。问题在于协调者单点、同步阻塞、提交阶段失败难以恢复,适合数据库资源较少、强一致要求高的内部系统。

TCC(Try-Confirm-Cancel)把业务拆成三个阶段:Try预留资源、Confirm确认执行、Cancel回滚。相比2PC,TCC不依赖数据库XA,性能更好,但业务侵入性强,每个操作都得实现三段逻辑。适合需要跨服务控制资源预留的场景,比如下单预占库存。

Saga适合长流程最终一致,每个本地事务成功就发布事件,失败则执行反向补偿。它没有全局锁,并发性能好,但隔离性弱,中间状态对外可见。我整理过一张对比表,面试直接用:

方案一致性业务侵入吞吐量适用场景
2PC强一致低(依赖资源管理器)低数据库少、强一致
TCC较强(业务保证)高(三阶段编码)中预占资源、跨服务写
Saga最终一致中(反向补偿)高长流程、允许中间态

5.4 幂等设计是分布式面试的保命题

幂等这个考点几乎必问,因为订单、支付、消息消费都会遇到重复请求。回答思路是给四层方案:

第一层,唯一索引。数据库表加业务唯一键,重复插入直接报错,最原始也最可靠。第二层,状态机。把订单状态流转做成有限状态机,只有当前状态允许的目标状态,才能执行对应操作,天然拒绝重复推进。第三层,token机制。前端提交前先获取token,后端处理时删除token,删除成功的请求才继续。第四层,去重表。将请求唯一ID插入一张去重表,插入失败说明重复,后续直接返回上一次结果。

这里要特别注意“消息消费幂等”的答法。不能只说“用Redis setnx”,要讲清楚:重复消费的根因是消费者处理超时后消息被重新投递;解决思路是消费前先记录消息ID到去重存储,消费完成后标记成功,下次相同消息ID直接跳过。最好补一句“去重记录本身要有过期时间,否则表会无限膨胀”。

6. 临场实战:26考点速查表与答题节奏

6.1 26考点总表:一句话答案与追问方向

整理了一份速查表,考前过一遍,检查自己哪些考点能接住追问,哪些只能说出名词。

编号考点一句话核心答案常见追问方向
1JVM内存分区堆、栈、方法区,线程私有与共享对象一定在堆上吗
2类加载机制双亲委派,加载、验证、准备、解析、初始化怎么打破双亲委派
3垃圾回收算法标记复制、标记清除、标记整理分代假设的依据
4G1/ZGC选型根据堆大小与延迟要求选择换ZGC需要哪些前置条件
5内存溢出排查先拿堆转储,再用分析工具定位对象引用链什么情况会MetaSpace溢出
6synchronized原理锁升级,JDK15后移除偏向锁锁对象头里存了什么
7ReentrantLock原理AQS队列同步器、ConditionAQS的state字段作用
8volatile语义可见性、禁止指令重排为什么不能保证原子性
9线程池参数核心、最大、队列、拒绝策略核心线程数怎么定
10CompletableFuture异步编排、exceptionally、allOfthenApply和thenCompose区别
11虚拟线程轻量线程、IO阻塞时卸载与线程池的适用边界
12HashMap原理数组+链表+红黑树、扰动函数为什么树化阈值是8
13ConcurrentHashMapCAS+synchronized锁桶size()是否准确
14fail-fast机制modCount检测CopyOnWriteArrayList代价
15字符串常量池编译期常量 vs 运行期intern两个字符串==比较的编译优化
16Record与不可变类语言级不可变载体与Lombok区别
17Sealed Class显式限定继承范围与final类、接口默认方法的区别
18索引与回表B+树、最左前缀、覆盖索引索引失效场景
19事务隔离级别四级隔离与并发问题映射可重复读如何实现
20MVCCundo log版本链+ReadView快照读与当前读的区别
21EXPLAIN调优type、key、rows、Extrafilesort怎么消除
22Redis持久化RDB快照+AOF日志+混合主从复制和持久化的关系
23缓存一致性Cache Aside、延迟双删并发窗口如何产生
24分布式锁SET NX EX+Lua释放主从切换锁丢失怎么办
25Bean生命周期属性填充、初始化、AOP代理循环依赖为什么三级缓存
26分布式事务2PC/TCC/Saga取舍幂等与分布式事务的关系

6.2 现场答题节奏:先说结论,再讲过程

面试答题最忌“从底层源码开始长篇大论”。正确的节奏是两步:先给结论或直接答案,再展开关键过程,最后补一个边界或踩坑点。

举个例子,面试官问“HashMap线程安全吗”。先说结论:“不安全,并发场景应使用ConcurrentHashMap。”然后补关键原因:“JDK 8之前的扩容头插法在并发下可能形成环形链表导致死循环,JDK 8改成尾插法后不再死循环,但并发put仍可能丢数据。”最后给个业务判断:“所以单线程、查询为主的场景可以用HashMap,并发写场景必须换工具。”整个回答控制在30秒内,信息密度足够,又给人“他真知道”的印象。

6.3 复习误区:只背不写、只看答案不反推

这几年看到的备考失败案例,几乎都栽在三个误区上。

第一个是只看答案不反推“为什么”。比如背了“ConcurrentHashMap的size()不准”,却不看CounterCell的设计动机,也解释不了“为什么不直接加锁读取”,一旦追问就露馅。

第二个是只背参数不写代码。线程池七个参数背得滚瓜烂熟,让手写一个带名字的ThreadFactory时直接卡壳。新特性题更是如此,Record和Sealed Class如果只在资料上看过,面试时连语法都写不对。

第三个是忽略版本差异。2026年的八股文如果还在说“偏向锁默认开启”,已经不符合标准JDK的新版本行为。备考时建议抓住JDK 17和21的LTS特性来看,重点是虚拟线程、模式匹配、可空性相关改进。我自己的做法是最近跟进到了JDK 26,手边开一个调试环境,遇到新特性顺手跑一下,语法印象比背书深得多。

最后分享一个私藏经验:考前别按资料目录刷,按“追问树”刷。把每个考点当成一棵树,根是定义,枝是原理,叶是踩坑案例。比如“Redis为什么快”的根是内存存储,枝是单线程模型和IO多路复用,叶是大Key阻塞、慢查询日志、持久化对性能的干扰。面到这个问题时,你自然能沿着树往下走,而不是背出“因为快”三个字就结束。2026年的面试,真正拉开差距的不是谁背得多,而是谁能把考点串成体系——这份26考点清单,就是帮你建体系用的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进