ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java后端面试八股文怎么答?从HashMap到Spring事务的实战套路

Java后端面试八股文怎么答?从HashMap到Spring事务的实战套路 最近总能在技术社区刷到“后端八股文”这个词尤其是Java后端几乎成了面试绕不过去的坎。有人吐槽它毫无意义有人靠它收割offer也有人背了一堆题还是挂得莫名其妙。作为一个面过不少候选人、也带过不少新人的Java后端开发我的看法其实很直接八股文本身不是问题问题在于你把八股文当成了背诵材料还是当成了思考框架。这篇博文不打算给你列一份“Java面试题大全”那东西网上一搜一大把。我更想聊的是怎么把那些老生常谈的HashMap、JVM、synchronized、Spring事务答出“这个人是真写过代码的”感觉——这才是“骚套路”的核心。本文适合正在备战Java后端面试的读者也适合刚入行想系统梳理技术体系的朋友它不会让你一夜变成架构师但能帮你把已有的知识组织成面试官愿意听的故事。1. 八股文被骂了这么多年Java后端面试为什么还是躲不掉1.1 门槛与筛选面试官为什么偏爱标准化问题先别急着骂。你需要理解面试官为什么要问八股文。Java后端的岗位供给远远大于需求一个hc放出去收到的简历可能有几百份真正能走到技术面的人依然很多。时间有限、候选人背景五花八门怎么在四十五分钟里快速判断一个人的基础扎实程度标准化问题是最经济的办法。这不是面试官偷懒而是筛选成本的问题。项目经历可以包装有的候选人能把一个简单的CRUD系统讲得天花乱坠但底层原理很难长期伪装。你问他HashMap的扩容机制、问synchronized锁升级、问Spring怎么解决循环依赖背后考察的是“你在写代码时有没有想过为什么”。如果能答出设计逻辑说明这个人写代码不只是“跑起来就行”如果只能背出结论那至少也知道他在什么层次。说白了八股文是面试官在低成本地筛选“会思考的人”。1.2 “骚套路”到底是什么套路所谓骚套路不是让你去背更偏的题、去赌面试官不会的冷门细节。恰恰相反是用一种更有经验感的表达方式把同样的知识点讲出差异化。我见过太多候选人问“HashMap原理”张口就是“数组加链表JDK8之后长度大于8变红黑树”然后戛然而止。这句话对吗对但毫无信息量。真正有经验的答法是分三层的先讲核心结构再讲为什么这么设计最后补一个自己实际用过的场景——比如“我之前在初始化Map时如果提前知道数据量会指定initialCapacity就是为了减少resize带来的性能损耗”。你看同样的知识点后者立刻有了“我确实在项目里优化过”的痕迹。这就是我一直强调的答题公式结论先行原理为主场景收尾。结论让面试官觉得你思路清晰原理证明你真懂场景证明你能落地。这套公式可以套用到几乎任何Java后端八股题上后面几章我会带你把高频考点一个个过一遍。2. Java基础考的不是语法是“你写代码时想没想过为什么”2.1 HashMap别急着背put流程先讲“2的幂”和0.75的来历HashMap是Java面试的“钉子户”几乎每一面都会遇到。但它也是区分度最高的一道题因为大多数人都只背了put的流程却讲不清楚背后的设计决策。put流程本身还是要完整的计算key的hash通过(n-1) hash定位桶位如果桶位为空直接放入不为空则遍历链表/红黑树找到相同key就覆盖没有就尾插链表长度达到阈值8且数组长度达到64时升级为红黑树元素个数超过size threshold时触发resize容量翻倍。但真正能体现功底的是为什么要用(n-1) hash而不是hash % n。因为当n是2的幂时(n-1) hash等价于取模运算但位运算比取模快得多。这个设计意味着扩容后每个元素的位置要么不变要么移动“原数组长度”的距离而不是全部重新散列。JDK8里利用了这个特性做了高低位拆分把原来的一条链表拆成两条一条留在原位一条移动到index oldCap。我在面试中问到这里十个人有六个会卡壳。再说加载因子为什么是0.75。这是一个在时间和空间之间取平衡的值。太小了比如0.5数组利用率低内存浪费严重太大了比如1.0虽然数组填满了才扩容但hash冲突概率急剧增加链表变长查询效率下降。0.75是统计学上一个相对均衡的选择。顺带一提链表转红黑树的阈值是8不是拍脑袋定的而是基于泊松分布计算出来的在加载因子0.75的前提下链表长度达到8的概率已经极低约千万分之六所以才敢在长度达到8时用红黑树换空间换性能。2.2 String的不可变性比“final修饰”值钱得多凡是答“String用final修饰class所以不可变”的基本都会被追问到怀疑人生。不可变性的设计意图才是面试官真正想听的。String不可变至少有四个层面的价值。第一是常量池的安全字符串字面量会被缓存并大量复用如果String可变一个引用改了值所有指向同一字符串的地方全部错乱第二是线程安全不可变对象天然可以在多线程环境下共享不需要加锁第三是hash缓存String的hashCode被缓存后适合做HashMap的key如果内容可变缓存就失效了第四是类加载机制的安全性类名就是字符串如果字符串可变恶意篡改类名会破坏ClassLoader的加载逻辑。面试里还有一个高频变体new String(abc)到底创建了几个对象。如果常量池里没有“abc”会创建一个常量池对象和一个堆对象共两个如果常量池里已经有了就只创建一个堆对象。这里有个容易犯迷糊的点String s abc只可能创建0个或1个对象但new String(abc)至少创建一个堆对象。我还遇到过候选人反问我“那intern()呢”这就是很好的加分互动你可以顺带解释intern会去常量池里找找不到就放进去并返回常量池引用而堆里的原对象不受影响。2.3 equals和hashCode的约定以及那些“一答就错”的坑这个考点几乎必考而且从基础题能一路追问到集合框架。核心约定很简单重写equals必须重写hashCode因为两个对象equals相等hashCode必须相等反过来hashCode相等不要求equals相等。但面试官通常会往坑里带。比如HashSet去重的完整过程其实涉及两层判断先算hashCode定位桶如果桶里没有元素直接放入如果桶里有元素再调用equals逐个比较equals也相等才判定重复。这就是为什么只重写equals不重写hashCode会导致HashSet里出现“逻辑相等但都存进去了”的脏数据。实际操作中我见过不少反模式。有人用对象里的可变字段参与hashCode计算比如用一个ArrayList成员变量做hash这个字段一旦被修改对象的hashCode就变了它在HashMap里的位置就找不到了——内存泄漏就是这么来的。还有人equals里用比较字符串字段两个内容相同的String用返回false导致明明业务上相等的对象判断为不等。我面试时会顺着问“那你在项目里为什么用Lombok的EqualsAndHashCode”其实是想引导候选人说“因为它会默认调用所有非静态非transient字段而且能识别继承结构”能意识到这层的人不多。2.4 受检异常、try-with-resources和泛型擦除的“冷饭热炒”异常体系和泛型看似基础但只要深入一点照样能拉开差距。受检异常checked exception和非受检异常unchecked exception的取舍我比较认同一句话如果一个异常是调用方可以通过合理手段恢复的就做成受检异常如果是程序bug或者不可恢复的就做成非受检异常。很多团队规范里干脆约定“业务异常继承RuntimeException”本质上是认为大部分异常在调用链里无法被有效恢复与其强制try-catch污染代码不如让框架统一处理。try-with-resources是JDK7引入的我建议每个Java后端都能讲透它。它的本质是语法糖凡是实现AutoCloseable的资源在try块结束后会自动调用close并且如果try块里和close都抛了异常原始异常会被保留close的异常会被压制suppressed。为什么这个设计重要因为如果不用它你需要自己写finally去关闭资源而finally里如果抛异常会覆盖原始异常排查问题的时候那个酸爽谁试谁知道。泛型的核心考点是类型擦除。Java的泛型是编译期概念运行时会擦除为原始类型。为什么Java不采用C#那种运行时泛型因为要保持JVM字节码向后兼容。这里最经典的追问是“那怎么能在运行时拿到泛型类型”答案是通过TypeReference或者反射获取泛型签名比如class UserDao extends BaseDaoUserBaseDao里通过getGenericSuperclass拿到ParameterizedType再拿ActualTypeArguments就能得到User这个类型。很多框架的BaseMapper就是这么做的。3. JVM别再背内存分区了把GC讲成一次线上OOM排查3.1 运行时数据区的重点不是画图是“哪个区域会怎么死”JVM内存区域是绕不开的但绝大多数人只会背堆、虚拟机栈、本地方法栈、方法区、程序计数器。这没用。你得知道每个区域异常时是什么表现。堆是对象分配的主战场堆溢出就是最常见的java.lang.OutOfMemoryError: Java heap space。虚拟机栈和本地方法栈溢出会抛StackOverflowError通常是递归没写终止条件。方法区在JDK8以后被移到了元空间Metaspace元空间默认使用本地内存如果加载的类太多会抛Metaspace相关的OOM。程序计数器是唯一不会OOM的区域因为它是线程私有的、只记录字节码执行地址。我面试时特别喜欢问一个场景线上有个Java进程内存一直在涨top一看CPU不高但内存快爆了你怎么办这题没有标准答案但我期待候选人能说出“先jmap -dump导出堆快照再用MAT分析Dominator Tree找大对象”这个链路。能答出这种排查链条的人说明他经历过线上事故而不只是看过书。3.2 GC与分代收集把“背算法”变成“讲设计”垃圾收集算法本身不难标记-清除、标记-复制、标记-整理。分代收集才是设计精髓。为什么新生代用复制算法、老年代用标记-整理核心假设是“朝生夕灭”大多数对象活不过第一轮GC。新生代里90%以上的对象都能被回收所以复制存活对象到另一块Survivor区成本很低但老年代里对象存活率极高如果用复制算法需要复制大量对象代价太大只能标记-清除或标记-整理。这套设计不是拍脑袋而是基于对对象生命周期的统计观察。G1和ZGC也经常被问到。G1的核心是“区域化”把堆分成一个个Region维护一个优先级列表每次回收价值最高的Region集合所以叫Garbage First。它能做到可预测的停顿时间通过-XX:MaxGCPauseMillis指定目标停顿时间G1会在成本模型里推算每次回收的Region数量。ZGC更进一步用染色指针和读屏障把停顿时间控制在10毫秒以内但代价是CPU占用更高和内存开销更大。回答这类问题关键在于体现出“你知道它解决了什么问题、付出了什么代价”而不是只背名词。3.3 类加载机制与双亲委派用“为什么破坏它”把知识串起来类加载的五个阶段——加载、验证、准备、解析、初始化——至少要能说出“加载”是找到字节码生成Class对象“准备”是给类变量分配内存并设置零值“初始化”是执行类构造器clinit。面试的高光点在双亲委派。双亲委派模型类加载器收到加载请求后不会自己先加载而是交给父加载器一直向上委派到Bootstrap ClassLoader父加载器加载不了再向下回落。为什么这么设计一是防止核心类库被篡改比如你写一个java.lang.String如果每次都让应用类加载器加载那JDK的String就被替换了二是避免同一个类被不同的类加载器重复加载保证类的唯一性。但双亲委派不是不能打破。Tomcat就打破了这个模型因为每个Web应用需要独立的类隔离不能互相污染Tomcat的WebAppClassLoader会优先自己加载Web应用下的类加载不到再去委托父加载器。JDBC的SPI机制也打破了双亲委派DriverManager在Bootstrap ClassLoader层但MySQL的Driver实现在应用类加载器里这违反双亲委派所以要靠ServiceLoader从线程上下文类加载器里加载驱动实现。能说到这个深度这道题基本就稳了。3.4 JVM调优面试官想听的不是命令是“定位问题的思路”JVM调优问到后期一般会聊到参数和工具。有些候选人能报出一大串-Xms、-Xmx、-XX:PrintGCDetails但问他“你线上遇到过什么问题、怎么定位的”瞬间卡壳。参数是死的定位思路才是活的。我自己的排查套路大致是先用top -H -p看进程CPU占用找出CPU飙升的线程再用jstack导出线程栈看是不是GC线程占满如果怀疑内存泄漏先用jstat -gcutil看GC频率和Eden/Survivor/Old的使用率判断是频繁Full GC还是堆膨胀然后jmap -dump:formatb,fileheap.hprof导出堆快照用MAT分析。一套链路下来问题基本就浮出水面了。我想强调一点JVM调优不是上来就改参数。很多内存“泄漏”其实是因为代码里把大对象长期引用GC永远回收不掉。你盲目调大堆内存只是把爆炸时间往后推迟。所以面试时如果聊到调优与其背参数不如讲一个你真实排查过的案例——哪怕是小demo级别的也比你报十个参数强。4. 并发题的高分姿势synchronized和CAS要讲出“锁的进化史”4.1 并发三大特性从CPU缓存讲到DCL单例并发编程的三个核心问题是原子性、可见性、有序性。大多数人都知道答案但不知道这三个词的真实来源。可见性的根源是CPU多级缓存和线程本地缓存。一个线程改了变量另一个线程可能看不到因为修改还在自己的缓存里没刷新到主内存。volatile做的事情就是禁用缓存强制每次读写都走主内存。有序性的根源是指令重排编译器和CPU为了流水线效率会调整指令顺序但多线程下可能造成意外结果。volatile通过内存屏障禁止了相关指令重排。原子性最简单也最容易被误解。i不是原子操作它包含读取、修改、写入三步。volatile不保证原子性因为它只解决了可见性和有序性。为什么经典的DCL双重检查锁单例要用volatile修饰instance因为instance new Singleton()不是原子指令它包含分配内存、构造对象、把引用赋值给instance三步如果发生指令重排可能出现另一个线程拿到一个“半初始化”的对象。volatile禁止了这个重排才能保证安全。面试时如果能把这个链条从硬件层面讲下来就已经赢过90%的人了。4.2 synchronized的锁升级从偏见到重量级的“进化史”synchronized最容易被轻视因为它是JDK层面的关键字很多人以为它天生就是“重量级锁”。但JDK6之后synchronized做了大量优化核心就是锁升级。升级路径是无锁 → 偏向锁 → 轻量级锁 → 重量级锁。偏向锁的意思是这个锁偏向第一个获取它的线程之后该线程再次进入时不需要任何CAS操作因为锁对象头里记录的就是它自己的线程ID。如果另一个线程来竞争偏向锁撤销升级为轻量级锁。轻量级锁本质是线程在自己的栈帧里创建一个锁记录用CAS尝试将对象头里的Mark Word替换成指向锁记录的指针。如果CAS失败说明存在真实竞争会进一步膨胀为重量级锁此时未获取到锁的线程会被挂起进入阻塞状态这是代价最高的状态。这个设计逻辑很好理解大多数情况下锁不仅不存在多线程竞争而且总是由同一个线程多次获得为了这种“无竞争”场景去挂起线程完全是浪费。所以用偏向锁和轻量级锁来兜底。面试里常追问的还有锁消除和锁粗化。锁消除是JIT编译时基于逃逸分析发现某个锁对象不会逃逸出当前方法就干脆消除同步锁粗化则是把连续加锁解锁的多个小同步块合并成一个大同步块减少反复加锁解锁的代价。4.3 CAS与AQS并发包的基石用“乐观锁”一秒钟讲懂Java并发包java.util.concurrent的设计基石一句话就能讲明白CAS相当于数据库里的乐观锁synchronized相当于悲观锁。CAS就是先比较内存里的值是不是预期的oldValue是就更新成newValue不是就失败整个过程是CPU指令级的原子操作。CAS常见的坑是ABA问题线程1读到值A线程2改成B又改回A线程1再次CAS时发现值还是A以为没人动过。解决方法是带版本号比如AtomicStampedReference。另一个问题是CAS自旋在竞争激烈时会空转消耗CPU。AQSAbstractQueuedSynchronizer是ReentrantLock、CountDownLatch、Semaphore这类同步器共同的骨架。核心就两样东西一个volatile的state表示同步状态一个CLH双向队列存放等待线程。以ReentrantLock为例线程抢锁就是CAS把state从0改成1拿到锁就执行拿不到就包装成Node排队前一个节点释放锁后会唤醒后一个节点。答这个题目时我建议画一个“排队等号”的生活类比state是柜台是否空闲的标志CLH队列是排队的队伍一切就清楚了。4.4 线程池七个参数不是用来背的是用来“设计”的线程池参数题我不会只让你背七个参数。我会让你设计一个线程池。七个参数是核心线程数、最大线程数、空闲存活时间、时间单位、任务队列、线程工厂、拒绝策略。执行流程也简单——提交任务时如果当前线程数小于核心线程数创建线程执行大于等于核心线程数丢进队列队列满了且线程数小于最大线程数继续创建线程达到最大线程数还无法处理触发拒绝策略。真正的加分点是“你为什么这么设置参数”。IO密集型和CPU密集型任务参数设置思路完全不同。CPU密集型任务核心线程数一般设置为CPU核数1IO密集型任务线程在执行IO时会阻塞等待所以可以多开几个线程常见经验值是CPU核数乘以2或者按CPU核数 / (1 - 阻塞系数)来估算。如果任务是短平快的队列可以设短一点如果是耗时任务队列要长一些否则很容易触发拒绝策略。还有两个实际工作中的坑。第一个是不要用Executors的快捷方法比如Executors.newFixedThreadPool底层用的是无界LinkedBlockingQueue任务多了会把内存撑爆newCachedThreadPool最大线程数是Integer.MAX_VALUE高并发下会创建海量线程。第二个是ThreadLocal的内存泄漏ThreadLocalMap的key是弱引用value是强引用线程不结束这个value就永远无法回收所以用完一定要remove。这个细节如果你能主动讲出来面试官会立刻觉得你是真踩过坑的。5. Spring面试的隐藏主线IoC容器、循环依赖和事务失效的底层逻辑5.1 IoC与Bean生命周期把“控制反转”从一个词讲成一出戏“Spring的核心是IoC和AOP”这句话差不多是个人都会说。但IoC到底反转了什么它的核心是对象创建权和依赖关系管理权从程序员手里移交给了容器。你不再自己new UserService()然后塞进UserController而是声明依赖容器帮你创建和注入。Bean的生命周期是经常考察的细节。我一般按七个阶段记BeanDefinition的解析与注册 → 实例化通过构造器→ 属性填充 → 初始化前的各种aware回调比如BeanNameAware、ApplicationContextAware→ BeanPostProcessor的postProcessBeforeInitialization → 初始化方法InitializingBean / init-method→ BeanPostProcessor的postProcessAfterInitialization → 使用 → 销毁。Spring的很多扩展点本质都是往这个生命周期里嵌钩子。这里有个设计上的加分点为什么Spring推荐构造器注入而不是字段注入字段注入的代码虽然写着方便但破坏了不可变性而且容易造成隐藏的循环依赖构造器注入能保证依赖在对象创建时就是完整的也方便单元测试里显式传参。Spring官方文档其实一直在引导这个方向如果你面试时主动说出这个观点至少说明你不是只会用Autowired。5.2 循环依赖三级缓存的“拆弹”思路循环依赖是Spring一个非常经典的设计题。A依赖BB依赖A如果按正常流程先创建A、A发现需要B、去创建B、B又发现需要A就死循环了。Spring的解法是三级缓存。一级缓存放创建完成的对象二级缓存放提前暴露的早期对象还没完成属性填充三级缓存放ObjectFactory也就是能生成代理对象的工厂。步骤是创建A时把A的ObjectFactory放进三级缓存然后填充属性发现需要B于是去创建BB填充属性时发现需要A于是从三级缓存里拿到A的ObjectFactory执行它得到A的早期引用放入二级缓存并注入BB创建完成后A再继续填充并拿到B最终A创建完成进入一级缓存。很多人会被“为什么需要三级缓存而不是二级”卡住。核心原因是代理。如果A需要被AOP代理那么最终容器里的A应该是一个代理对象。如果只有二级缓存提前暴露出去的是普通对象后面再想换成代理对象就晚了。三级缓存里的ObjectFactory可以在创建早期引用时就执行getEarlyBeanReference提前生成代理对象保证暴露出去的就是正确的代理。能把这个层次讲明白面试官基本会停止追问。5.3 AOP与Transactional失效高频陷阱一次说全AOP的原理分两个层面概念层面是切面、切点、通知、连接点实现层面是动态代理。Spring Boot 2.x之后默认用CGLIB而Early Spring时代默认用JDK动态代理。原因很简单JDK动态代理只能代理接口CGLIB通过字节码生成子类来代理不需要接口。CGLIB的代价是不能代理final类和方法但绝大多数业务类没有final限制。Transactional为什么失效是面试里出现频率极高的故障排查题。常见的失效场景至少有五种自调用导致代理失效同类里方法A调用方法BB上有Transactional但因为是this调用根本没走代理对象事务不生效。解决方法是注入自身代理或者拆分到另一个Bean。方法不是publicSpring的AOP默认只代理public方法private、protected方法上标注事务不生效。异常被catch掉了事务回滚靠的是异常传播你catch住异常不抛出Spring感知不到自然不回滚。rollbackFor没配置默认只对RuntimeException和Error回滚如果抛的是受检异常不会回滚。数据库引擎不支持事务比如MySQL的MyISAM引擎就不支持事务,得是InnoDB。我在前面的帖子也提过一个排查技巧如果事务没生效先确认日志里有没有生成代理对象再看方法是public且被外部调用最后看异常配置。这套排查思路比背失效场景更能体现实战能力。5.4 Spring Boot自动配置与Spring MVC入门必问但深度不够Spring Boot的自动配置高频题是SpringBootApplication到底干了什么。它由三个注解组合而成SpringBootConfiguration表明这是一个配置类EnableAutoConfiguration开启自动配置ComponentScan扫描当前包及其子包下的组件。真正有深度的是EnableAutoConfiguration的实现原理。它的核心是通过AutoConfigurationImportSelector去加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里的所有自动配置类。每个自动配置类上又有一堆ConditionalOnClass、ConditionalOnMissingBean之类的条件注解只有满足条件才生效。比如RedisAutoConfiguration会在classpath里存在RedisTemplate类、容器里没有自定义RedisTemplate时生效。所以“自动配置”不是真的全自动是“条件满足才配”。Spring MVC的请求流程也是高频题请求先到DispatcherServletDispatcherServlet通过HandlerMapping找到对应的HandlerController方法然后通过HandlerAdapter调用执行前会经过拦截器链Controller返回结果后经视图解析或直接ResponseBody序列化返回。前后端分离之后基本不会有视图解析这一步了都是返回JSON由Jackson等序列化器把对象转成JSON字符串。顺便说一句遇到“前端无法获取数据”这类问题排查顺序一般是先看接口返回是否正常再看跨域配置再看前端调用参数是否对得上——这三个环节是最容易出问题的地方。6. MySQL和Redis的高频题怎么答出“我真的在项目里用过”6.1 MySQL索引B树为什么能成为默认选择不夸张地说索引题答得好不好基本能决定后端面试的最终评价。B树为什么吊打B树和哈希索引这个必须讲清楚。哈希索引的查找复杂度是O(1)但无法支持范围查询也无法支持最左前缀排序所以只能用于Memory引擎等特定场景。B树每个节点都存数据树的高度更低但范围查询需要中序遍历而且每个节点能存的子节点数量受数据大小影响。B树把所有数据都放在叶子节点非叶子节点只存索引键这样每个节点能存更多键树更矮磁盘IO更少叶子节点之间用链表串联范围查询直接顺序扫描叶子链表效率极高。再往深一点聚集索引和普通索引的区别。InnoDB的聚集索引就是主键索引叶子节点直接存整行数据普通索引的叶子节点存的是主键值所以通过普通索引查询时需要先找到主键再回聚集索引再查一次这个过程叫回表。为了减少回表可以设计覆盖索引——把查询需要的字段都放进索引里这样就不用回表了。最左前缀原则则要求你在建联合索引时把最常查询的字段放左边。面试时如果能主动说一句“我会用explain看执行计划关注type是不是range/ref/const、有没有用到索引、有没有filesort”面试官一般会高看一眼。别小看这一句它代表你查过慢SQL而不仅仅是背过索引理论。6.2 MySQL事务与MVCCRR级别下幻读到底解决没有事务的ACID是基础隔离级别是进阶MVCC是拉开差距的地方。MySQL默认隔离级别是REPEATABLE READ可重复读实现机制主要靠MVCC和锁。MVCC的核心是undo log版本链和ReadView。每行记录有隐藏列trx_id事务ID、roll_pointer回滚指针。每次修改都会生成一条undo log版本链就串起了一个行的历史版本。事务读数据时根据一定规则生成ReadView判断版本链中哪个版本对当前事务可见。这就是快照读的实现基础。幻读的定义是在一个事务里两次范围查询结果集不一样出现了一些“幽灵记录”。InnoDB在RR级别下对于当前读比如select ... for update、update、delete会用Next-Key Lock来解决幻读。Next-Key Lock是记录锁加间隙锁的组合既锁住了已有的记录也锁住了记录之间的间隙让其他事务无法在这个范围内插入新记录。所以严格说RR级别下InnoDB通过MVCC解决了快照读的幻读通过Next-Key Lock解决了当前读的幻读。能分清楚“快照读”和“当前读”这两个概念这段话才讲得完整。6.3 Redis缓存穿透、击穿、雪崩怎么答出“实战感”Redis的高频题缓存穿透、缓存击穿、缓存雪崩这“三兄弟”几乎必考。很多人会背定义但说不到落地方案。穿透是缓存和数据库都没有数据请求直接打到数据库。常见方案有两个一是布隆过滤器先把所有可能存在的数据的hash放入布隆过滤器请求来了先过过滤器不存在直接返回二是把空值也缓存起来设置一个较短的过期时间比如30秒。布隆过滤器的问题是有误判率但能挡住绝大多数无效请求。击穿是某个热点key过期瞬间大量请求同时打向数据库。解决办法是互斥锁同一个key只允许一个线程去查数据库其他线程等待后读缓存。另一种思路是“逻辑过期”缓存里不设置物理过期时间而是写入一个逻辑过期时间戳后台线程异步刷新这样缓存永远存在不会出现击穿窗口。雪崩是大量key在同一时间过期导致数据库被瞬间打爆。方案就是对过期时间做随机化比如在基础过期时间上加上一个随机值避免大规模同时失效还可以做多级缓存本地缓存加Redis打到数据库的流量再降一层。答这道题时我建议顺便提一句缓存一致性更新数据时先更新数据库再删除Redis缓存也就是Cache Aside模式这是最容易被接受也最简单的方式先删缓存再更新DB会有旧数据回填缓存的风险除非加上延迟双删这类补偿策略。6.4 Kafka为什么能支撑百万并发Kafka本来就是热点技术词特别是“为什么能支撑百万并发”这类问题。它的高性能来自几个设计顺序写磁盘磁盘顺序写的速度远超随机写接近内存随机读的水平页缓存Kafka重度依赖操作系统的Page Cache读写都走页缓存命中率高的时候几乎不碰磁盘零拷贝通过sendfile系统调用直接在内核态完成文件到网卡的传输省掉了用户态和内核态之间多次拷贝分区机制一个topic拆成多个partition分布在多台broker上并行读写能力自然上升。再加上消息批量发送和压缩单机吞吐量自然惊人。回答这类中间件问题关键是抓住“性能和扩展性来自哪几个层面”别东一句西一句。7. 临场发挥从“背题机器”到“看起来像老工程师”的练习法7.1 对着镜子“讲题”而不是对着电脑“背题”这是我自己备战面试时最有效的一招把每个高频考点写成问题清单然后不看答案自己对着镜子或者录音工具讲一遍。注意是“讲”不是“背”。如果讲到一半卡壳了说明这个知识点还没真正消化如果讲完后听录音发现逻辑混乱说明还得重新整理结构。更好的办法是找一个不懂Java的朋友给他讲HashMap的原理。如果你能用生活化的类比让他听明白“数组加链表”“扩容搬家”“为什么0.75”这些概念说明你才是真懂了。如果只能甩术语那你的理解还停留在表面。这其实就是“费曼学习法”在Java面试准备里的落地——把复杂知识讲给小白听是对理解程度最残酷的检验。7.2 用“问题树”把零散知识点串成体系很多人准备八股文是刷题式的今天看HashMap明天看JVM后天看Spring知识点都是碎片。真正到了面试现场面试官从“线程池”追到“AQS”再追到“CAS”你就容易断片。我建议你用“问题树”的方式整理。选几个核心入口从根节点一层层往下展开。比如从“JVM”出发下面分内存区域、垃圾回收、类加载、调优工具垃圾回收下面再分判定对象是否存活可达性分析、垃圾收集算法、垃圾收集器、GC日志怎么看。每个节点都准备一个三层回答是什么、为什么、我遇到过什么。这样面试官不管从哪个节点往上问往下问你都能沿着树迁移。7.3 遇到不会的题如何体面地把“不知道”变成机会没有人能答对所有问题面试官自己也做不到。所以重点不是“全部答对”而是“不会的时候怎么反应”。我认为比较体面的做法是坦诚加思路。先承认自己没深入了解过然后尝试从已有知识里推导。比如面试官问“你了解ZGC的实现细节吗”你确实只听过名字可以这样答“ZGC的具体实现细节我没有深入看过但我知道它主打超低停顿核心思路是并发标记和染色指针本质上还是用空间换时间。如果是线上系统需要亚毫秒级停顿我会优先考虑它但当前我还没遇到这种极端场景。”你看先把丑话说在前面然后展示你的推断逻辑最后拉回自己的经验范围这比死撑着瞎编正派得多。面试官也是工程师他欣赏的是诚实但有思路的人。瞎编被追问出来比说不会糟糕十倍。最后分享一个我个人准备面试时的习惯每次面试结束后我会把没答好的问题记下来不管是用备忘录还是笔记软件然后给每个问题打上标签比如“并发”“JVM”“Spring”。过一段时间再翻出来你会惊讶地发现当初让你卡壳的问题其实都指向同一块薄弱区——那块区域才是你真正要补的地方。准备Java后端八股文这件事说到底不是背答案而是借这个机会把自己的技术体系补成一张完整的网。网织好了面试只是顺手的事。
返回列表