尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Java面试核心考点:吃透JVM、并发、集合与Spring底层原理

Java面试核心考点:吃透JVM、并发、集合与Spring底层原理 每年到这个时间点总能在技术社群里看到同一种景象不管校招还是社招大家手里都攒着几份PDF文件名里写着“终极版”“精华版”“面试必背”点开一看几百道题答案背得滚瓜烂熟。但真到了面试现场一问“为什么这么设计”很多人就卡住了。我这些年既面过别人也被别人面过慢慢发现一个很反直觉的事实八股文本身没有错错的是把它当成“背诵材料”而不是“理解提纲”来用。面试官真正想听的不是你背出来的标准答案而是你推导出这个答案的逻辑链。所以这篇总结我打算换一种写法。同样是Java面试的核心知识点我不只给你答案还会把每个考点背后的“为什么需要它”“它到底解决了什么问题”“如果你来设计会怎么考虑”一起串起来。这也是一份面向2026届春招、秋招以及社招冲刺的复习地图建议先收藏再按章节逐步消化。1. 面试前先想明白这份总结该怎么用才不会变成死记硬背先说个我自己的观察。去年帮一个学弟做模拟面试他简历上写着“熟悉JVM调优”我问“你们项目里遇到过Full GC吗怎么排查的”他第一反应是背出JVM参数列表-Xms、-Xmx、-XX:UseG1GC背得很完整。但当我接着问“为什么这两个参数要设成一样大”时他愣了几秒说“网上建议这么配”。这就是典型的“背答案但没理解答案”。其实“为什么-Xms和-Xmx要一样大”这个问题的核心是避免运行期堆扩容带来的性能抖动和对象地址变动。JVM在启动时如果初始堆很小随着对象增多会触发扩容扩容过程涉及堆内存重新划分和对象移动会造成明显的停顿。生产环境直接初始化到最大堆就是拿空间换稳定。类似的例子在Java面试里到处都是。所以这篇总结里我会有意把每个考点拆成三层是什么一句话能讲清楚的定义用来快速过知识点。怎么用代码层面、配置层面、场景层面的具体体现。为什么设计者当初面临什么问题为什么选这个方案有没有替代方案。你复习的时候不要光盯着“是什么”层那层只是用来建立骨架。真正让你和竞争者拉开差距的是“为什么”层。面试官问问题的深度通常也是沿着这个方向走的先问定义再问用法最后追到底层原理和设计取舍。另外提醒一句不要试图“刷完”这份内容。它覆盖的是Java面试中最高频、最容易被追问的板块。更合理的用法是先快速通读一遍把所有考点看个眼熟然后针对自己不熟悉的板块单独建文档用自己的话把答案写出来。写不出来的部分才是你真正需要补的地方。2. JVM核心考点内存区域、垃圾收集与类加载关键是能画出自己的逻辑链JVM这块在Java面试中出现频率极高而且几乎每一家都会让你说说内存区域。但很多人只背了“堆、栈、方法区”六个字一追问就散架。这块我建议你把知识串成一条逻辑链程序跑起来数据放哪里满了怎么办谁负责清理怎么清理。2.1 内存区域为什么要分堆和栈答到这个层面才算过关虚拟机栈、本地方法栈、程序计数器、堆、方法区这五个区域怎么区分大部分人都能背出来。但真正能体现功力的是能不能说清楚为什么会这样划分。核心原因有三个维度第一线程隔离。栈和程序计数器是线程私有的每个线程执行到哪一行、调用栈有多深必须自己维护。堆和方法区是线程共享的所以才有并发访问和加锁的问题。第二生命周期差异。栈里的栈帧随方法调用创建、随方法返回销毁生命周期短且确定堆里的对象什么时候没人用了需要GC来判断生命周期长且不确定。把这两类数据混在一起管理会非常低效。第三管理方式不同。栈是连续内存空间只需移动栈顶指针就能分配和释放速度极快堆需要动态分配、垃圾回收、内存碎片整理复杂得多。理解了这三点面试官如果再追问“JDK8里方法区去哪了”你就能顺势讲出元空间Metaspace取代永久代的背景永久代放在堆内大小难以估算很容易出现OutOfMemoryError: PermGen space同时永久代参与Full GC拖慢回收效率。而元空间直接使用本地内存默认只受物理内存限制字符串常量池也移到了堆里。这一改本质是把“难以估量的元数据”从“需要精细管理的堆”里挪出去。2.2 垃圾收集算法和收集器从标记-清除到G1/ZGC怎么讲才有层次垃圾收集这块很多人的回答停留在“标记-清除、标记-复制、标记-整理”三个算法名字上再问收集器就只能背出CMS、G1。要拿到高分需要把“算法”和“收集器”的关系说清楚。收集器是具体实现算法是收集器内部某个阶段采用的策略。比如CMS它的初始标记和重新标记用到了STWStop The World并发标记阶段用的就是“可达性分析三色标记”最终清理阶段则涉及标记-清除算法的变体。而G1整体上采用的是“分区复制”的思路把堆划分成多个大小相等的Region每个Region都可以独立回收这样就能做到“可预测的停顿时间”。面试官很喜欢接着问“G1和CMS相比到底强在哪”。你可以从三个角度答回收策略CMS是老年代收集器需要配合ParNew使用G1是面向整个堆的收集器不需要分代配合。碎片问题CMS用标记-清除会产生内存碎片最终触发Full GC时停顿很长G1用复制算法回收后Region内是紧凑的基本没有碎片。可预测性G1通过维护一个优先级列表每次根据用户设定的预期停顿时间-XX:MaxGCPauseMillis选择回收收益最大的Region集合这个设计思路叫“垃圾优先”。CMS不提供这种停顿时间预测。如果你面的是高阶岗位还可以补充一句ZGC或Shenandoah的染色指针与读屏障技术它能做到十毫秒以内的暂停。但注意不要为了炫技硬提一个自己讲不透的概念面试官一旦追问“你项目中用过吗”答不上来反而扣分。稳妥的做法是G1讲透ZGC点到为止。2.3 类加载机制与双亲委派不光背流程还要能解释“为什么非要这么干”类加载这个考点标准答案是“加载、验证、准备、解析、初始化”五个阶段。但我觉得更值得重视的是双亲委派模型因为它背后是一个典型的安全和隔离设计。双亲委派的核心逻辑是一个类加载器收到加载请求时不会自己先尝试加载而是把请求委派给父加载器。只有当父加载器反馈无法完成加载时子加载器才尝试自己加载。这么设计至少有两个关键原因防止核心API被篡改。比如你自己写了一个java.lang.String如果不经过委派启动类加载器在加载String时就会加载到你写的这个类整个JVM就乱套了。有了双亲委派系统类库永远由启动类加载器加载自定义类不会污染核心类库。避免类的重复加载。同一个类如果两个不同的加载器都加载了一遍在JVM里会被当成两个不同的类用instanceof判断时会出问题。委托给父加载器统一加载可以保证同一个类在全系统中只会被加载一次。面试官如果继续深化可能会问“Tomcat为什么要打破双亲委派”。Tomcat需要同时部署多个Web应用每个应用可能依赖不同版本的Spring、不同版本的类库如果还要严格遵循双亲委派所有Web应用共享同一个类加载器那版本冲突就无解了。所以Tomcat为每个Web应用创建一个独立的WebAppClassLoader并且优先加载自己WEB-INF/classes和WEB-INF/lib下的类加载不到再交给父加载器。这个例子是“理解双亲委派”最好的延伸题。3. 并发编程连环问volatile、synchronized、AQS、线程池要从机制到场景串起来并发是Java面试题里的“深水区”。很多候选人能说出几个关键字但问到底层就含糊。我个人复习这个板块的心得是不要单独背某个知识点而是用“线程间如何协作、数据如何保证可见、锁如何竞争”这条主线把所有点串起来。3.1 JMM和volatile为什么靠内存屏障而不是靠锁Java内存模型JMM规定了主内存和工作内存的交互规则。简单说每条线程有自己的工作内存缓存了主内存变量的副本线程对变量的操作只能在工作内存中进行不能直接读写主内存。这就带来了三个并发问题可见性、原子性、有序性。volatile能解决可见性和有序性但解决不了原子性。这个结论每个看过八股的人都能说出来但你能解释“volatile为什么能保证可见性”吗关键在于内存屏障。在volatile变量的写操作前后JVM会插入屏障指令强制把当前线程工作内存中修改的值刷新到主内存在读操作前后强制从主内存重新读取。同时屏障还会禁止指令重排序这就是有序性的来源。举个例子单例模式中常见的双重检查锁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即使synchronized保证了原子性和可见性但instance new Singleton()这一步在指令层面会被拆成三件事分配内存、初始化对象、把引用指向内存。由于指令重排序第三步可能先于第二步执行。此时另一个线程第一次判空发现instance不为null直接返回了一个尚未完成初始化的对象。这就是volatile在这里存在的全部意义。3.2 synchronized的锁升级过程这个细节最常被追问以前很多Java面试题还停留在“synchronized是重量级锁”但自从JDK 6做了锁优化之后这个说法就过时了。现在的标准答案是无锁 → 偏向锁 → 轻量级锁 → 重量级锁叫锁升级。整个过程是这样演进的初始时没有任何线程竞争对象头里的Mark Word记录的是无锁状态。当第一个线程访问同步块时JVM把对象头的锁标志位改为偏向锁记录这个线程的ID。偏向锁的意思是“这个锁偏向于第一个获得它的线程”如果后面还是同一个线程来访问就不需要再做任何同步操作性能极高。一旦有第二个线程来竞争偏向锁就会被撤销升级为轻量级锁。轻量级锁通过自旋CAS尝试获取锁不阻塞线程适合锁持有时间很短的场景。如果自旋失败或竞争进一步加剧轻量级锁会升级为重量级锁。此时未获取锁的线程会进入阻塞状态由操作系统调度涉及用户态和内核态的切换代价最大。面试时如果能主动说出“偏向锁在JDK 15之后被默认禁用、JDK 18中已被标记为废弃”这个演进趋势会显得你对Java版本变化很敏感。因为偏向锁在如今的高并发场景下收益越来越低撤销偏向锁带来的STW代价反而不可忽略所以HotSpot团队逐步把它“边缘化”了。3.3 AQS的底层逻辑ReentrantLock和Semaphore的公共答案AQSAbstractQueuedSynchronizer是Java并发包里最核心的类。ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock都是基于它实现的。面试官问AQS本质上是在问“并发工具类的公共底座是怎么设计的”。你可以这么回答AQS维护了一个volatile int类型的state变量和一个FIFO双向等待队列。state的含义由子类自己定义比如ReentrantLock里state表示锁的重入次数Semaphore里state表示剩余许可证数量。线程获取锁时通过CAS来修改state如果修改失败说明资源被占用当前线程就会被包装成一个Node节点加入等待队列尾部并通过LockSupport.park阻塞自己释放资源时把state改回去然后唤醒队列头部的等待线程。这里有个高频追问“ReentrantLock和synchronized的区别有哪些”可以用表格来总结维度synchronizedReentrantLock锁获取方式隐式进入同步块自动获取显式调用lock()/unlock()是否可中断不可中断lockInterruptibly()支持中断是否可超时不支持tryLock(timeout)支持公平性非公平可指定公平锁条件变量Object的wait/notifynewCondition()支持多个条件队列底层实现基于Monitor基于AQS需要注意的是虽然ReentrantLock功能更丰富但synchronized在JDK 6之后经过锁升级优化简单场景下性能并不差而且写法更简洁、不易出错。我在实际项目里会优先用synchronized只有需要可中断、可超时、多个条件队列时才换ReentrantLock。3.4 线程池七个参数怎么记拒绝策略怎么选线程池为什么必须掌握因为稍大一点的项目都会用到而且面试官很容易从“你们项目里线程池怎么用的”一路问到底层。七个参数分别是核心线程数corePoolSize、最大线程数maximumPoolSize、空闲存活时间keepAliveTime、时间单位unit、任务队列workQueue、线程工厂threadFactory、拒绝策略handler。记忆口诀其实很简单“两个人、一个队、一个工厂、一个策略”。但比记参数更重要的是理解线程池的完整工作流程。用大白话讲线程池创建后来一个任务先看当前线程数是否小于核心线程数小于就创建新线程执行。当线程数达到核心线程数后新任务进入任务队列排队。当队列也满了再看线程数是否小于最大线程数小于则继续创建新线程这部分线程就是非核心线程。如果线程数已经达到最大线程数队列也满了就只能走拒绝策略。拒绝策略有四种默认是AbortPolicy直接抛异常。我在线上项目中最常用的是CallerRunsPolicy让提交任务的线程自己执行这个任务既不会丢任务又起到天然“限流”的作用让提交方感受压力从而放慢提交速度。DiscardPolicy一定要谨慎因为它是静默丢弃线上出问题很难排查。另外阿里Java开发规范里推荐用ThreadPoolExecutor的方式显式创建线程池不要用Executors提供的快捷方法。原因很直接Executors.newFixedThreadPool和newSingleThreadExecutor的队列是默认无界的LinkedBlockingQueue任务堆积会导致内存暴涨newCachedThreadPool的maximumPoolSize是Integer.MAX_VALUE高并发下会创建大量线程直接拖垮系统。这些坑在面试中也很容易被拿出来当反面案例。4. 集合框架连问ArrayList与HashMap是面试入口却不是终点集合这块看起来简单但它是面试官最爱的“连环问”起点。ArrayList的问法能从构造器问到扩容再到fail-fast机制HashMap能从结构问到put流程再到红黑树为什么阈值是8。我建议你带着“设计者视角”去复习而不是只背源码行号。4.1 ArrayList扩容为什么是1.5倍而不是两倍ArrayList底层是Object数组默认容量10。每次扩容时通过int newCapacity oldCapacity (oldCapacity 1)计算新容量也就是原来的1.5倍然后调用Arrays.copyOf把旧数组内容拷到新数组。这个1.5倍并不是拍脑袋定的。如果扩容倍数太小比如1.1倍那每次添加元素都可能触发扩容频繁拷贝数组性能很差。如果扩容倍数太大比如2倍或3倍虽然扩容次数少了但一次性占用的内存空间更多可能造成内存浪费。1.5倍是一个折中值既保证扩容次数不会太频繁又不会让数组容量增长过快。顺着这个话题面试官经常会追一个常见问题“ArrayList和LinkedList有什么区别”常规回答是“ArrayList基于动态数组查询快、插入慢LinkedList基于双向链表插入快、查询慢”。但如果你能补充“在实际项目中LinkedList的随机访问是O(n)所以遍历不要用get(i)要拿迭代器而ArrayList的尾部插入因为扩容机制均摊时间复杂度还是O(1)另外LinkedList每个节点有额外的前后指针内存占用比ArrayList大很多”就会显得你既有理论知识又有实践感知。4.2 HashMap的put流程和红黑树阈值怎么讲才能让面试官点头HashMap是Java面试中“最卷”的题目需要讲清楚的点有底层结构、hash扰动、扩容机制、红黑树转换条件。我建议按下面的顺序回答底层结构是数组链表红黑树。数组的每个位置叫桶bucket当多个key的hash值映射到同一个桶时以链表形式存储链表过长时转为红黑树。计算索引时先用key的hashCode做一次扰动计算h ^ (h 16)。这一步是把高位16位的信息混入低位因为HashMap计算桶下标用的是(n - 1) hash如果n比较小直接使用原始hash会让高半区的信息全部丢失增加碰撞概率。扰动函数的目的就是让高半区也参与散列。put流程是先判断数组是否为空为空则扩容再根据hash计算桶下标如果桶为空直接放入不为空则判断链表头节点是否匹配匹配则覆盖不匹配则遍历链表链表长度超过8且数组长度超过64时转为红黑树。扩容时数组容量变为原来的两倍。JDK 8对扩容做了优化因为新容量是旧容量的2倍所以元素在新数组中的下标要么是原下标要么是“原下标旧容量”。这个结论取决于(n - 1) hash中新增的那个高位bit。JDK 8通过判断hash的该bit来决定元素是否需要移动避免了JDK 7中重新计算hash再插入的低效做法。红黑树阈值为什么是8这个问题的答案要从泊松分布说起。Java源码注释里说在随机hashCode下桶中元素个数达到8的概率非常低大约千万分之一所以设置成8既不会让树化频繁发生又能防止极端碰撞场景下的性能退化。同时树化还需要数组长度达到64否则即使链表很长也先扩容因为扩容后链表会被拆散可能不需要树化。4.3 ConcurrentHashMap的分段锁到CASsynchronized演进逻辑比代码更重要面试官问你ConcurrentHashMap本质上是在考察“对并发性能与一致性之间权衡的理解”。JDK 7的ConcurrentHashMap使用Segment分段锁整个Map被分成多个Segment每个Segment内部是一张哈希表不同Segment可并发操作但同一Segment内仍然串行。锁粒度是Segment理论上并发度等于Segment数。JDK 8做了重大升级放弃Segment直接用Node数组加锁粒度细化到单个桶锁对象是桶的头节点。在桶为空的时候用CAS无锁插入在桶不为空时再用synchronized锁住头节点进行插入或更新。这里用synchronized而不是ReentrantLock是因为JDK 6之后synchronized经过锁升级优化在低竞争场景下开销极小同时语法更简洁维护成本更低。与此同时JDK 8的ConcurrentHashMap和HashMap一样在链表过长时也会转红黑树用TreeBin包装红黑树根节点同样支持并发读写。还有一个细节值得提扩容时支持多线程协助迁移即多个线程可以一起帮忙把旧数组的元素搬到新数组迁移完成后才允许其他线程操作。这个“协助扩容”机制也是面试中考察比较深的问题。5. Spring核心机制Bean生命周期、循环依赖、AOP是三个绕不开的坎Spring是Java后端岗位的标配几乎每一轮技术面都会问。但很多人只是背了大概流程一旦面试官追问细节就露馅。我发现最有效的复习方式是把Bean的“一生”从头到尾走一遍然后把循环依赖和AOP挂到这条生命周期线上形成有因果关系的整体。5.1 Bean生命周期从定义到销毁中间经历的每个阶段都要知道Spring容器启动后Bean的创建过程大致是扫描或读取配置得到BeanDefinition。实例化Bean通过构造器反射创建对象实例。属性填充对标注了Autowired、Resource等注解的属性进行依赖注入。执行Aware接口回调例如BeanNameAware设置Bean名字、BeanFactoryAware注入BeanFactory、ApplicationContextAware注入ApplicationContext。执行BeanPostProcessor的前置方法postProcessBeforeInitialization。执行初始化逻辑如果Bean实现了InitializingBean接口调用afterPropertiesSet如果配置了init-method调用指定方法。执行BeanPostProcessor的后置方法postProcessAfterInitialization。这一步也是Spring AOP创建代理对象的关键时机。此时Bean就绪可以被正常使用。容器关闭时销毁Bean如果实现了DisposableBean调用destroy如果配置了destroy-method调用指定方法。很多人会问这个流程面试时必须一字不差吗我的建议是记住主要脉络然后用例子说话。比如你可以主动说“我们项目里有个缓存预热的组件就是在afterPropertiesSet里加载配置到内存的。如果用BeanPostProcessor可以在每个Bean初始化后都做增强适合做统一日志收集。” 这样显得你不仅背了流程还在实际工作中用过。5.2 循环依赖三级缓存到底在解决什么问题Spring解决循环依赖靠的是三级缓存这可能是Java面试中“背诵率”最高的问题之一。三个缓存分别是一级缓存singletonObjects存放已经完成整个创建流程的单例Bean。二级缓存earlySingletonObjects存放提前暴露的早期Bean引用也就是实例化完成但还没完成属性填充和初始化的对象。三级缓存singletonFactories存放ObjectFactory负责生成早期Bean的代理对象。为什么需要三级而不是两级关键点在于如果某个Bean被AOP代理那么在循环依赖时提前暴露给其他Bean的必须是代理对象而不是原始对象。三级缓存里存的是ObjectFactory它内部会判断这个Bean是否需要AOP需要就返回代理对象不需要就返回原始对象。如果只有两级缓存在属性填充阶段就要把原始对象放进去但那时还没执行BeanPostProcessor代理对象还没生成后面再补代理就会出现“注入的是原始对象”的问题。这里有个容易被追问的边界构造器注入的循环依赖无法被三级缓存解决。因为构造器注入发生在实例化阶段Bean还没创建出来无法提前暴露引用。这类循环依赖只能用Lazy延迟加载来打破。还有一个边界是prototype作用域的Bean不缓存也解决不了循环依赖。5.3 AOP是动态代理的包装理解底层才能应对追问Spring AOP的核心是动态代理。默认情况下如果目标类实现了接口Spring使用JDK动态代理如果没有实现接口则使用CGLIB代理。JDK动态代理基于接口通过Proxy.newProxyInstance生成实现类对象CGLIB通过继承目标类来生成子类重写目标方法。面试中常见的问题是“JDK动态代理和CGLIB的区别”。可以从三个维度回答实现方式JDK基于接口CGLIB基于继承。限制JDK代理要求目标类必须有接口CGLIB可以代理没有接口的类但无法代理final类和方法。性能JDK 8之后JDK代理的创建和调用性能已经很好Spring Boot默认在目标类没有接口时使用CGLIB无需额外配置。Spring Boot 2.x之后官方也推荐使用CGLIB。AOP的原理可以与Bean生命周期关联起来在Bean初始化完成后BeanPostProcessor会检查这个Bean是否匹配切点表达式如果匹配就生成代理对象并替换原来的Bean。这也是Spring AOP和AspectJ的最大区别Spring AOP在运行时生成代理AspectJ在编译期或类加载期织入。如果面试官再问“为什么Spring事务有时候会失效”答案也藏在AOP里自调用时this.method()调用不会经过代理对象所以事务注解不会生效。解决办法是通过AopContext.currentProxy()获取代理对象或者把调用拆到另一个Bean中。这是非常贴近实际工作的一类问题值得多花几分钟理解。6. MySQL与Redis高频点索引结构、事务隔离与缓存一致性是“Java岗必附带题”你会发现Java岗面试几乎不会只问Java本身。尤其是后端岗位MySQL和Redis基本属于“公共科目”。这块内容如果能在Java面试里答出深度会明显拉高面试官对你的评价。6.1 索引为什么选B树而不是B树或者红黑树MySQL的InnoDB存储引擎使用B树作为索引结构。面试官问“为什么是B树”最佳回答方式是做一组对比对比红黑树/AVL树它们是二叉树随着数据量增大树的高度会迅速增加。假设几百万行数据树高可能是20层以上每层对应一次磁盘I/O性能会出问题。B树的每个节点可以存储很多key能够把树高控制在3~4层。对比B树B树每个节点既存索引也存数据同一高度下能存储的索引数量更少而且中序遍历需要递归访问多个节点不适合范围查询。B树的所有数据都存在叶子节点并且叶子节点之间用指针串成有序链表做范围查询时只需找到起点然后沿着链表顺序读取效率极高。对比哈希索引哈希索引等值查询O(1)很快但不支持范围查询和排序所以InnoDB默认还是用B树。如果面试官继续深挖“聚簇索引和二级索引的区别”你可以说InnoDB的聚簇索引主键索引叶子节点直接存放整行数据所以通过主键查询只需要一次索引查找二级索引叶子节点存放的是主键值通过二级索引查询到主键后还需要回表到聚簇索引再查一次这个过程叫回表。覆盖索引就是二级索引包含了查询需要的所有字段可以避免回表是性能优化的重要手段。6.2 事务隔离级别与MVCC锁与版本链的配合MySQL的事务隔离级别有四种读未提交、读已提交、可重复读、串行化。默认隔离级别是可重复读Repeatable Read。**MVCC多版本并发控制**是实现读已提交和可重复读的核心机制。在InnoDB中每行记录都有隐藏列事务IDDB_TRX_ID和回滚指针DB_ROLL_PTR。每次更新操作不会直接覆盖旧数据而是生成一个新版本并通过回滚指针串成版本链。读操作怎么决定读哪个版本依靠ReadView。ReadView记录了生成时刻活跃事务的ID列表。读已提交和可重复读的区别就在于生成ReadView的时机不同读已提交每次执行SELECT都生成新的ReadView所以能读到其他事务已提交的新数据可重复读在第一次SELECT时生成ReadView之后一直复用所以事务内多次读取结果一致。这个设计可以说非常巧妙普通的快照读不用加锁通过版本链和ReadView实现一致性快照大幅提高了并发读的性能。当前读比如SELECT ... FOR UPDATE、UPDATE、DELETE则走的是加锁逻辑会用到记录锁、间隙锁、Next-Key Lock。关于间隙锁有一个容易踩坑的点在可重复读隔离级别下InnoDB使用Next-Key Lock记录锁间隙锁来防止幻读这意味着对一个范围内的记录加锁时会同时锁住这个范围内不存在的“间隙”影响插入操作。生产环境高并发插入场景下如果出现了大量锁等待很多时候就跟间隙锁有关。6.3 缓存的三个经典问题穿透、击穿、雪崩面试问Redis穿透、击穿、雪崩几乎是三个标配问题。它们的本质区别可以用一句话记缓存穿透查一个不存在的数据缓存和数据库都没有请求直接打到数据库。缓存击穿一个非常热点的key在缓存过期的瞬间大量请求同时打到数据库。缓存雪崩大量key在同一时间段过期或者Redis实例宕机导致大量请求打到数据库。针对穿透常见方案有三种缓存空值设置较短的过期时间、布隆过滤器拦截、参数校验。布隆过滤器尤其值得讲一下它用多个哈希函数把key映射到bitmap上判断某个key一定不存在时就直接返回不用访问数据库。它的缺点是“可能存在误判”但不存在“漏判”对穿透场景很适合。针对击穿方案是热点key不设置过期时间或者后台异步更新缓存也可以用互斥锁只让一个线程去查数据库并回填缓存其他线程等锁后直接读缓存。分布式场景下可以基于Redis的SETNX实现互斥锁。针对雪崩方案是过期时间加随机值避免大量key同时过期使用多级缓存本地缓存Redis对请求做限流降级主从架构加哨兵保障Redis高可用。7. 临场答题的策略同样是背过为什么有人能拿Offer有人挂最后这部分不聊具体知识点了聊一个更实际的问题知识点都过了一遍面试时怎么把“我会”变成“面试官觉得我会”。我参与过不少面试发现候选人答案质量的分水岭往往不在于知识面广度而在于三点第一先给结论再给理由。面试官问“HashMap线程安全吗”不要一上来就开始背源码。先说“不安全多线程put可能导致死循环或数据覆盖”再补一句“JDK 7中头插法在多线程扩容时会形成环JDK 8改成尾插法解决了环的问题但数据覆盖问题依然存在”。这种“总-分”结构让面试官能在十秒内判断你懂不懂后续追问才有意义。第二用项目说话。很多面试题看似是纯概念题其实都可以和项目结合。比如问线程池参数怎么设置你答完公式还不够最好补一个真实例子“我们线上是I/O密集型任务核心线程数设为CPU核数的两倍左右队列用有界ArrayBlockingQueue拒绝策略用CallerRunsPolicy防止任务无界堆积。” 面试官听到这个会认为你是在用知识解决问题而不是在复述课本。第三不会的题目不要硬编。有两类处理方式区别很大。一类是直接说“这个我没深入了解过”然后就没下文了这当然不好。另一类是换个角度说“这个概念我没有生产实践但凭我对JVM的理解它的原理可能是这样……如果有偏差希望您指正。” 第二种回答即使不准确也体现了你的推导能力和诚实。大多数面试官对这类回答是给正分的。这里我还想单独提一下复习方法。不要用“刷题数量”来评判准备程度而是用“能不能不看资料讲出逻辑链”来检验。你可以每天抽一到两个板块拿一张白纸画一下它的核心流程。比如今天就画“Spring Bean生命周期”明天画“HashMap put流程”后天画“线程池执行流程”。凡是画不出来的环节就是对这块知识还有盲区。这种方法比反复看笔记有效得多我试过也用这个方法帮过不少人反馈都很好。另外准备面试时不要只盯着知识点本身也要顺手查一下相关版本的更新。比如你简历上写了“熟悉Java”最好知道JDK当前最新LTS版本里有哪些改动能聊。这篇总结里的锁升级、ConcurrentHashMap实现等都是跟版本演进强相关的如果你在面试时能主动提到设计变化的原因很容易给面试官留下“这人不是死背书”的印象。复习这件事其实没有捷径但一定有路径。把每个考点从“看到答案认识”变成“合上资料能讲”是一层再从“讲清楚是什么”变成“说清楚为什么这样设计”是更深的一层。这篇总结能做的就是帮你把主干和逻辑铺好剩下的要靠你自己把这些内容变成自己的话在面试现场从容地讲出来。
返回列表