2026年Java后端面试的激烈程度,不用我多说了。很多人一边刷LeetCode一边背八股文,最后却挂在"为什么"上。所谓"八股文",其实是Java面试里那些结构化、高频重复、有标准答案的基础知识点,从HashMap扩容到JVM垃圾回收,再到Spring AOP代理。这篇文章我就把近一年面试里反复出现、我觉得最值得背的内容按模块整理成了一份速查级汇总,每个问题都附带可以直接说出口的答案和容易踩的坑。
1. 面试八股文到底在考什么
1.1 为什么问这么多基础题
很多人一提到"八股文"就反感,觉得面试官在刁难人。但站在面试官的角度想:项目经验可以包装,业务代码可以背,但基础题最难装。问一句"HashMap在JDK 8里做了什么优化",立刻就能看出你是背了面试题,还是真跑过代码、看过源码。
Java基础题考的从来不是"记性",而是三样东西:一是对底层原理的理解深度,二是把复杂问题讲清楚的能力,三是知识体系是否成网。比如问你volatile,如果只回答"保证可见性",那只是背了一个名词;如果能继续说出"JMM内存模型、指令重排、内存屏障、为什么不能保证原子性",这才算真正掌握。
1.2 怎么背才能不白背
我的建议是分三层来准备:第一层是"知道是什么",比如能说出AQS的全称是AbstractQueuedSynchronizer;第二层是"理解为什么",比如为什么AQS要用双向队列、为什么状态用volatile修饰;第三层是"讲得出细节",比如加锁失败后节点怎么入队、怎么唤醒后继节点。
三层都过一遍,才算把一道题吃透。这里有一个很笨但很有效的方法:把每一道高频题当作一道"小讲课",假装自己在给同事讲原理,用口语讲一遍,卡壳的地方就是你没真正理解的地方。面试前如果能对着空气讲清楚十道题,上考场就不会慌。
2. Java核心基础高频题:集合、异常、反射与泛型
2.1 集合框架:ArrayList和HashMap是基础也是坑
集合这块十个面试官有九个会从HashMap开始问。先把最核心的几道题过一遍。
问题1:HashMap在JDK 7和JDK 8之间有什么变化?
答案要点:JDK 8引入了红黑树。当链表长度超过阈值8且数组容量大于等于64时,链表转为红黑树,目的是把查询复杂度从O(n)降到O(log n)。新增节点从头插法变成了尾插法,JDK 7的头插法在并发扩容时会形成环形链表,导致死循环,虽然现在大家都不推荐在多线程下用HashMap,但作为历史知识点还是经常被问到。
问题2:HashMap的put流程是怎样的?
这个问题一定要能完整说出来:先对key的hashCode做扰动计算,也就是高16位和低16位异或,然后通过(n - 1) & hash来定位到数组下标;如果下标位置为空,直接放入Node;不为空就判断key是否相同,相同则替换value;否则判断当前节点是不是树节点,是树节点就走红黑树插入,不是就遍历链表,链表长度超过8且数组长度大于等于64就树化;最后判断size是否超过threshold,超过就扩容。
这里容易被追问的是:为什么要用异或扰动?因为直接使用hashCode参与取模,散列效果通常不好,把高16位与低16位异或能让高位信息也参与进来,从而降低哈希碰撞概率。另外一个高频追问是"为什么HashMap容量是2的幂",因为只有容量是2的幂时,(n - 1) & hash才等价于hash % n,而且位运算比取模快得多。
问题3:ConcurrentHashMap的并发控制是怎么实现的?
JDK 7采用的是Segment分段锁,把整个Map分成16个Segment,每个Segment是一把可重入锁,理论上支持16个线程并发写。JDK 8废除了分段锁,改用CAS加synchronized:首先在插入时如果对应桶为空,就用CAS把节点放进桶里,避免加锁;如果桶不为空,则对桶里的头节点加synchronized锁;同时提供了LongAdder类似的计数思路来维护size。这个演进方向体现的其实是"锁粒度越来越细"的思路,回答时如果能把这个趋势说出来,会很加分。
整个集合框架里,ArrayList、LinkedList、HashSet、TreeMap也经常出现,但核心始终是HashMap和ConcurrentHashMap。建议自己动手画一遍存储结构图,把数组、链表、红黑树、扩容后的迁移过程画出来,比干背文字管用。
2.2 异常与反射:常被追问的底层原理
异常这块看似简单,但问得细很容易露馅。几个必背题目:
问题1:受检异常和非受检异常有什么区别?
受检异常(checked exception)必须显式捕获或往外抛,编译器会强制检查,典型的有IOException、SQLException;非受检异常(unchecked exception)是RuntimeException及其子类,比如NullPointerException、IllegalArgumentException,编译器不检查。判断标准很简单:继承自Exception但不继承自RuntimeException的是受检异常,继承自RuntimeException的就是非受检异常。
问题2:Error和Exception有什么区别?
两者都继承自Throwable。Error一般是JVM层面的严重问题,比如OutOfMemoryError、StackOverflowError,程序通常无法恢复,不应该去捕获;Exception是程序运行层面的问题,可以通过代码处理。面试官常追问"OOM是Error还是Exception",答案是Error。
问题3:try-with-resources的原理是什么?
JDK 7引入的语法糖,使用实现了AutoCloseable接口的资源时,会在try块结束后自动调用close方法。它解决的是传统try-finally里资源关闭代码冗长、且finally里close又抛异常会把原始异常吞掉的问题。反编译后会看到编译器自动生成了close调用和suppressed异常的记录逻辑。
反射也是高频区域。最常问的是"反射有什么用"和"反射为什么慢"。反射可以在运行时获取类的完整结构、调用私有方法和私有字段,Spring IOC、动态代理、注解处理都依赖反射。性能慢的原因比较复杂:方法调用时需要做权限检查、参数解析,很多反射调用没有办法做JIT热点优化,还会触发类加载过程中的一些额外开销。日常开发里能用接口或者函数式编程替代反射的地方,尽量别用反射。
2.3 泛型:面试里最容易答空的一块
泛型在面试里被问得不如HashMap多,但一旦问到,很多人都会卡壳。
问题1:Java的泛型是怎么实现的?
Java的泛型是基于擦除机制实现的。编译时就能捕捉类型错误,但对类型变量做了类型擦除,编译过后Class文件中只能看到原始类型。也就是说,List 和List 在运行时是同一个类型List。泛型信息只存在于编译阶段,所以泛型类不能持有类型信息,不能直接new T()。
问题2:? extends T和? super T有什么区别?
这其实是PECS原则(Producer Extends, Consumer Super)。? extends T表示"某个T的子类型",适合只读场景,因为编译器不知道实际的元素类型是什么,不能往里添加元素;? super T表示"某个T的父类型",适合只写场景,因为往里添加T及其子类型都是安全的,但往外读取时只能读取Object类型。常见例子:Collections.copy方法就同时用到了extends和super。
问题3:为什么不能创建泛型数组?
因为数组在运行时是协变的,String[]可以赋值给Object[],如果泛型也能创建数组,擦除后类型信息丢失,堆污染问题很难被约束。举个例子,new List [10]这个操作如果允许,那么在类型擦除后变成new List[10],一个List 也能放进去,数组在运行时的类型检查就会失效,破坏类型安全。所以编译器直接拒绝这个操作。
3. JVM与内存管理:面试官最喜欢的深水区
3.1 JVM内存区域划分:一张图记住运行时数据区
JVM题目几乎是高级开发和校招的必考项。首先要熟练掌握运行时数据区。
- 堆(Heap):对象实例和数组分配的主要区域,也是垃圾回收的主战场,通过-Xms和-Xmx控制大小。
- 虚拟机栈(JVM Stack):线程私有,每个方法调用都会创建一个栈帧,栈帧里存放局部变量表、操作数栈、动态链接、方法出口。栈深度不够时抛StackOverflowError。
- 本地方法栈(Native Method Stack):为native方法服务,线程私有。
- 程序计数器(Program Counter Register):保存当前线程执行字节码的行号,属于线程私有,是唯一不会OOM的区域。
- 方法区/元空间(Method Area/Metaspace):存放类信息、常量、静态变量。JDK 8用元空间替换永久代,元空间使用的是本地内存。
对象创建流程也要能讲清楚。先说类加载检查,然后为对象分配内存,分配方式有指针碰撞(Bump the Pointer)和空闲列表(Free List)两种,具体取决于堆是否规整,而堆是否规整又取决于垃圾回收器是否带压缩整理功能。接下来是并发安全问题,有两种处理方式:一种是对分配空间的动作做同步处理,另一种是本地线程分配缓冲(TLAB)。最后是初始化零值、设置对象头、执行构造方法。
3.2 垃圾回收算法与收集器:把停顿时间聊明白
垃圾回收是JVM的绝对重点。三个基础算法:
- 标记-清除:先标记可回收对象,再统一回收。缺点是产生大量内存碎片,标记和清除效率都不高。
- 标记-复制:把内存分成两块,只使用其中一块,回收时把存活对象复制到另一块。适合新生代这种"存活对象少"的场景,缺点是浪费一半空间。
- 标记-整理:标记完存活对象后,把所有存活对象向一端移动,然后清理边界外的内存。适合老年代,没有碎片,但移动对象的开销大。
HotSpot默认采用分代收集,新生代用复制算法,老年代用标记-整理或标记-清除。面试常问的新生代划分是Eden区和两个Survivor区,默认比例8:1:1,每次Minor GC之后存活对象在Eden和两个Survivor之间不断复制,年龄达到阈值进入老年代。
收集器对比也是高频题。Serial是单线程,适合客户端;Parallel Scavenge关注吞吐量;CMS是并发收集器,目标是低停顿,但会产生内存碎片;G1是JDK 9以后的默认收集器,把堆划分成多个Region,可以指定最大停顿时间,用回收价值最高的Region优先回收;ZGC的目标是把停顿时间控制在10ms以内,使用的是着色指针等新技术。如果面试官问"你了解ZGC吗",能说出"基于区域、不分代、可达性分析用染色指针标记、支持TB级别堆、STW时间极短"就算过关。
3.3 类加载机制与双亲委派:为什么不能重写String
类加载过程分五步:加载、验证、准备、解析、初始化。加载是找到Class文件字节流并生成Class对象;验证是校验字节码安全性;准备是为静态变量分配内存并设置零值;解析是把常量池里的符号引用替换为直接引用;初始化是执行类构造器 方法。
双亲委派模型是另一个必问点。启动类加载器(Bootstrap)负责加载JDK核心类,扩展类加载器(Extension)和应用程序类加载器(Application)逐级向下。当一个类需要被加载时,会先委托父加载器加载,父加载器加载不了才轮到子加载器。这样做的核心目的是保证类的唯一性,防止用户自定义java.lang.String来覆盖JDK核心类。
"有没有打破过双亲委派"这个问题,最常见的例子是JDBC的SPI机制。JDBC的DriverManager是由启动类加载器加载的,但它需要加载第三方数据库驱动,而第三方jar包在classpath下,启动类加载器根本加载不到。所以JDBC驱动加载采用了线程上下文类加载器,在运行时动态指定加载路径,这就在局部打破了双亲委派。Tomcat为了隔离不同Web应用的类,也会用专门的WebAppClassLoader优先加载应用目录下的类。
3.4 JVM调优与线上排查:OOM真没那么神秘
调优题一般结合场景问。最典型的问题是"假如线上OOM你怎么排查"。我比较推荐的回答路径是:先用jps找到进程号,用jmap -heap看一下堆使用情况,用jmap -dump:format=b导出堆转储文件,再用MAT或者VisualVM分析大对象;如果是CPU飙高,用top -Hp找到高CPU线程,再用jstack导出线程快照,把线程号转成十六进制后去栈里找对应的线程状态,一般能看到GC线程或者业务线程的bug。
常见OOM类型要记牢:堆内存溢出对应java.lang.OutOfMemoryError: Java heap space;元空间溢出对应Metaspace;栈溢出对应StackOverflowError;还有一类是创建线程太多导致的unable to create new native thread。回答这些时如果能顺带说出"GC overhead limit exceeded"这种冷门类型,会显得你真的遇到过。
我不建议死记一堆JVM参数,但要能说出常用的几个:-Xms设置初始堆大小,-Xmx设置最大堆大小,-Xmn设置新生代大小,-XX:MaxMetaspaceSize设置元空间上限,-XX:+HeapDumpOnOutOfMemoryError表示OOM时自动导出堆快照。面试时讲到具体排障场景,配合这些参数说出来会很有说服力。
4. 并发编程:必考中的必考
4.1 线程基础与synchronized/volatile:从状态到锁升级
并发编程是Java面试的"深水区",也是拉开差距的主要模块。
线程的生命周期必须先掌握:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)、等待超时(Timed Waiting)、终止(Terminated)。注意Java线程状态在Thread.State里定义,实际只有NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED六种,RUNNABLE状态包含了操作系统层面的就绪和运行。
synchronized在JDK 6之后有锁升级过程,答题顺序是:偏向锁、轻量级锁、重量级锁。偏向锁会记录持有线程的ID,适合无竞争场景;一旦发生竞争就升级为轻量级锁,线程通过CAS自旋尝试获取锁;如果自旋失败或线程数增加,就膨胀为重量级锁,依赖操作系统互斥量,阻塞和唤醒之间要切换内核态,开销很大。JDK 15之后偏向锁被废除,但面试题库里的旧题目仍然存在,回答时点一句"偏向锁后续版本已经废弃"反而加分。
volatile要重点讲清楚两点:可见性和有序性。可见性是因为写volatile变量时,JMM会插入内存屏障,强制把工作内存里的修改刷回主内存,并使其他线程里该变量的缓存失效。有序性是可以禁止指令重排,典型的应用是单例模式的DCL双重检查锁,单例字段加volatile可以防止指令重排导致拿到半初始化的对象。但volatile不能保证原子性,比如count++操作,本质上是读-改-写三步,volatile只能保证这三步的可见性,不能保证三步的原子性。
4.2 JUC的核心组件:Lock、AQS、并发工具
ReentrantLock和synchronized的区别是必考题,可以从四点回答:ReentrantLock可以响应中断、支持超时、支持公平锁、可以绑定多个条件队列。底层实现依赖AbstractQueuedSynchronizer(AQS)。
AQS是JUC的基石,很多工具类都建立在它之上。AQS内部维护了一个volatile的state变量和一个CLH变体双向等待队列。以ReentrantLock为例,抢锁就是通过CAS把state从0改成1,抢锁失败就把当前线程封装成Node节点加入队列尾部,然后通过LockSupport.park挂起线程;解锁时把state改回0,然后唤醒队列里的头节点后继线程。公平锁和非公平锁的区别在于:非公平锁在进入AQS之前会先直接CAS抢一次锁,抢不到再排队;公平锁严格按队列顺序获取。
CountDownLatch和CyclicBarrier也经常一起问。CountDownLatch是计数器减到0就放行,用一次就没用了;CyclicBarrier是让一组线程互相等待到齐后再继续,可以循环复用。Semaphore是信号量,控制同时访问资源的线程数,可以拿来做限流。面试官如果追问"AQS是怎么实现共享模式的",能说出Semaphore和CountDownLatch用的是共享锁就够。
CAS也一定会被问到。CAS是Compare And Swap,通过Unsafe类提供的本地方法实现,核心是用一个期望值和一个目标值去比较内存中的实际值,如果一致就替换,否则重试。CAS避免了线程阻塞,但有两个典型问题:ABA问题和自旋CPU开销。ABA可以用AtomicStampedReference解决,给变量加版本号;自旋开销大则是因为在高竞争下CAS会不断重试。
4.3 线程池:七个参数与拒绝策略
线程池简直是面试出场率最高的题,必须把ThreadPoolExecutor七个参数背得滚瓜烂熟:核心线程数corePoolSize、最大线程数maximumPoolSize、空闲线程存活时间keepAliveTime、时间单位unit、等待队列workQueue、线程工厂threadFactory、拒绝策略handler。
任务提交后的执行流程是:先判断当前线程数是否小于核心线程数,小于就创建新线程执行任务;否则把任务放入队列等待;如果队列满了,再判断当前线程数是否小于最大线程数,小于就创建新线程;如果已经达到最大线程数,就执行拒绝策略。这里有一个很多人都会记混的点:是先放队列,再到最大线程数,而不是先创建到最大线程数再放队列。
四种拒绝策略分别是:AbortPolicy直接抛异常(默认)、CallerRunsPolicy调用者线程直接执行、DiscardPolicy直接丢弃、DiscardOldestPolicy丢弃队列里最老的任务。
线程池参数怎么设置,这是个开放题。CPU密集型任务可以把核心线程数设置为CPU核数加一;IO密集型任务可以设置为CPU核数乘以二,或者用公式:CPU核数除以(1 - 阻塞系数)。如果是混合型任务,更好的做法是拆分任务或者用多个线程池分别处理。回答时最好提到"不要写死,要根据压测结果动态调整"。
5. Spring、数据库与Redis:项目背后的实力
5.1 Spring IOC/AOP、事务:把Bean生命周期讲透
Spring框架在Java面试里的比重越来越高,尤其社招面试很少只问纯语言,一定会结合项目问框架原理。
IOC和DI:控制反转是指对象的创建和依赖关系的维护由容器来完成,而不是由对象自己new,核心思想是把控制权从代码转移到容器。依赖注入是实现控制反转的方式,有构造器注入、Setter注入、字段注入三种方式。面试中常被追问"Bean的生命周期",完整的生命周期包括:实例化、属性填充、初始化前回调(如BeanPostProcessor的postProcessBeforeInitialization)、初始化方法、初始化后回调(postProcessAfterInitialization)、销毁。其中AOP动态代理就是在BeanPostProcessor阶段生成的代理对象。
AOP的底层机制要分清楚JDK动态代理和CGLIB。JDK动态代理要求目标类实现接口,通过Proxy.newProxyInstance生成代理对象;CGLIB通过生成目标类的子类来完成代理,不要求实现接口。Spring Boot 2.x默认会用CGLIB代理。AOP概念里的切面、切点、通知、连接点,至少要能用自己的话说清楚。
Spring事务:事务传播行为有七种,最常问的是REQUIRED和REQUIRES_NEW。REQUIRED表示支持当前事务,如果没有就新建;REQUIRES_NEW表示挂起当前事务,新建一个独立事务。Spring事务失效的几个常见场景也经常考:方法不是public、自调用、发生异常被捕获、抛出受检异常而事务默认只回滚RuntimeException、数据库不支持事务机制等。自调用问题的本质是事务通过代理对象生效,类内部方法调用走的是this,不经过代理。
5.2 Spring Boot自动装配与Spring MVC:请求到底走了哪条路
Spring Boot的自动装配原理也是大热题。答案可以从@SpringBootApplication入手,它由@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan组成。@EnableAutoConfiguration通过@Import导入AutoConfigurationImportSelector,该类会扫描所有jar包里的META-INF/spring.factories文件,读取里面配置的自动配置类,再按条件注解(@ConditionalOnXxx)逐个判断是否生效。
Spring MVC的处理流程要能画出来并说清楚:请求先到DispatcherServlet,由它调用HandlerMapping找到处理器和拦截器,再通过HandlerAdapter适配执行Controller方法,返回ModelAndView,经过视图解析器解析后渲染。现在前后端分离的项目基本都返回Json,视图渲染环节可以简化,但DispatcherServlet、HandlerMapping、HandlerAdapter这三个核心组件一定要说明白。
过滤器和拦截器的区别也问得很多。过滤器是Servlet规范层面的,作用于所有请求,在DispatcherServlet之前执行;拦截器是Spring MVC层面的,只作用于Controller请求。过滤器先执行,拦截器后执行,两者都可以做登录校验、日志记录,但拦截器可以拿到HandlerMethod等信息,所以业务上用得更多。
5.3 MySQL索引与事务隔离级别:从B+树到MVCC
数据库方面,索引是首要重点。InnoDB用的是B+树索引,为什么不是B树、不是红黑树?关键的几个理由:B+树的非叶子节点只存索引不存数据,单个节点能存储更多键值,树高更低,减少磁盘IO;叶子节点之间用指针连接,适合范围查询;所有数据都在叶子节点,查询路径长度固定,性能稳定。要能顺着这个思路把"为什么选择B+树"讲透。
索引还要掌握聚簇索引和二级索引的区别。InnoDB的主键索引就是聚簇索引,叶子节点直接存储整行数据;二级索引的叶子节点存储的是主键值,所以通过二级索引查询数据时往往需要回表。如果要查的字段正好都在二级索引里,就能避免回表,这叫覆盖索引,是SQL优化里很常用的一招。
事务隔离级别与MVCC:四种隔离级别是读未提交、读已提交、可重复读、串行化。MySQL InnoDB默认是可重复读。MVCC(多版本并发控制)通过隐藏字段(trx_id、roll_pointer)和undo log实现,读已提交和可重复读的区别在于生成ReadView的时机:读已提交每次SELECT都生成新ReadView,可重复读只在第一次SELECT时生成。这就是为什么可重复读能解决幻读的原理基础,但InnoDB可重复读下幻读的彻底解决依赖间隙锁。
锁:InnoDB锁的类型包括共享锁、排他锁、表锁、行锁、间隙锁和next-key lock。行锁种类中,记录锁锁住单条记录,间隙锁锁住间隙,next-key lock同时锁记录和间隙,InnoDB默认的RR级别下会用它来防止幻读。
5.4 Redis缓存基础:穿透、击穿、雪崩一次讲完
Redis基本不会缺席Java面试。首先要能说出五种基础数据结构:String、Hash、List、Set、ZSet,以及各自的典型场景。再往下问,可能会问到底层编码和跳表。ZSet用跳表加哈希表实现,跳表支持范围查询,插入删除的时间复杂度是O(log n),实现难度比平衡树低。
缓存三类经典问题是穿透、击穿、雪崩:
- 缓存穿透:请求的数据缓存和数据库里都不存在,每次请求都会打到数据库。解决方法是缓存空值、布隆过滤器。
- 缓存击穿:某个热点key过期瞬间,大量请求同时打到数据库。解决方法是互斥锁、逻辑过期。
- 缓存雪崩:大量key在同一时间过期,或Redis宕机,导致数据库压力瞬间增大。解决方法是过期时间加随机值、热点数据不过期、多级缓存、高可用集群。
持久化方面,RDB是某个时间点的一次全量快照,文件紧凑,恢复快,但可能丢失最后一次快照之后的数据;AOF是追加操作日志,数据丢失少,但文件大、恢复慢。两者可以同时开启,或以AOF为主。面试时如果能提一句"4.0之后AOF支持混合持久化,RDB作为base数据加上增量AOF日志,兼顾加载速度和数据安全",会让面试官眼前一亮。
6. 场景题与算法:八股文之外真正的分水岭
6.1 场景设计题怎么答:先定边界,再选型
很多基础题背得好的人,在场景题这里突然卡壳。因为场景题考的不是记忆,而是工程思维。我的建议是记住一个回答框架:先明确需求边界,再估算规模,然后选型,最后排查风险和扩展性。
拿"设计一个短链服务"举例:先问清楚是给内部用还是外部用、每秒QPS多大、短链有效期多久;然后估算数据量,比如每天1000万条新链接,一年3.6亿条,要用什么存储;选型上短链生成的本质是发号器,可以用数据库自增主键加进制转换,也可以用预生成号段;接着要考虑的是重定向用301还是302,防攻击的限流怎么做,过期清理怎么做。把流程讲完,比给出一个唯一正确答案重要得多。
其他高频场景题还有:分布式锁怎么实现、接口幂等怎么做、消息丢失和重复消费怎么解决、秒杀系统怎么设计。分布式锁可以回答基于Redis的SETNX加过期时间、基于ZooKeeper的临时顺序节点。接口幂等可以用唯一索引、状态机、Token机制。消息队列的可靠投递要讲清楚生产端confirm机制、消费端手动ack、消息表、重试和死信队列。
6.2 高频算法题怎么准备:别只会刷题
算法题不是八股文,但算法题准备得怎么样,往往决定你能不能进到后面的技术面。我的建议是不要盲目刷题,先把高频类型吃透:数组和双指针、字符串处理、链表操作、二叉树遍历、回溯、动态规划、排序和二分。
排序算法中考得最多的是快排、归并、堆排序和基础的冒泡排序。冒泡排序虽然实际工程里几乎不用,但作为入门题经常出现在笔试和面试里,尤其是让手写并分析时间复杂度。要能快速写出稳定版冒泡:外层循环控制轮数,内层循环相邻比较,每轮把最大元素冒到末尾,时间复杂度O(n²)。在此基础上如果能说出优化点(某一轮没有发生交换就提前结束),就算过了。
我见过不少候选人,LeetCode刷了几百道,但让他写一个单例模式都写不完整。所以算法之外,手写题里"单例模式、生产者消费者、手写一个LRU、手写线程安全的计数器"这四类高频代码要多练几遍。特别是LRU,最好能用LinkedHashMap实现一版,再用双向链表加HashMap实现一版,面试官看到你能写出第二种,通常会很满意。
7. 备考策略与经验分享
7.1 面试题背下来就够吗
说句实话,面试越来越难不是因为题难,而是因为面试官越来越会"顺着问"。你背了一个结论,他马上追问一个"为什么",再追问一个"你项目里遇到的是什么情况"。所以只背题面答案远远不够,一定要把每个结论背后的原理链条打通。
有一些题是可以"准备故事"的。比如你简历上写了Redis缓存,那就至少准备好三个故事:缓存穿透是怎么出现的、怎么排查的、怎么解决的;Redis挂了之后系统有没有降级方案;缓存和数据库的一致性是怎么保证的。如果在讲项目时能自然引出这些细节,比单纯背Redis的八股文要有说服力得多。
7.2 学习路线与时间安排建议
最后给一个我自己的备考节奏,仅供参考。总共四周到六周比较合理。
第一周:把Java基础、集合、泛型、异常过一遍,同时每天刷两道LeetCode热题。第二周:主攻JVM和并发,把内存模型、垃圾回收、AQS这些硬骨头啃下来。第三周:主攻Spring、MySQL、Redis,优先看自己简历上写过的东西。第四周:刷场景题和高频算法,同时开始做模拟面试,找人互相问,或者自己录音回听。第五周到第六周如果没有时间可以不做,但如果面试时间允许,再针对性补弱项。
补充一条个人心得:整理错题比刷题重要。我面试准备期间会维护一份错题文档,把回答卡壳的题目、被追问倒的题目全部记录下来,当天晚上重新讲一遍。有时候一顿饭的功夫就能把一个薄弱点补齐,比闷头背一天书高效得多。
我个人在实际准备面试时还有一个习惯:在面试前一晚把所有核心知识点浓缩成一张"一页纸",不追求答案完整,只记录关键词和触发点。比如AQS后面写"state、CLH、LockSupport、CAS",HashMap后面写"扰动、位运算、树化、尾插"。第二天候场时快速过一遍,思路会清晰很多。这个方法我也推荐给你,八股文可以背,但更重要的是在面试官面前展示出你是一个能独立思考、真正理解底层的工程师。