ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Java面试高频考点全梳理:从String到分布式锁的完整备战指南

Java面试高频考点全梳理:从String到分布式锁的完整备战指南 从第几轮面试被问到“String和StringBuilder到底选哪个”开始我一个做Java多年的老家伙居然答得支支吾吾那次经历把我彻底打醒了。后来每次帮人模拟面试、每次自己换工作只要遇到值得记的题我都会往收藏夹里塞一条。几年下来这份Java开发面试常见问题的清单越攒越长也确实帮好几个朋友拿到了心仪的offer如今我还在持续往里加内容。这篇文章就把目前沉淀下来的一些高频问题和一个完整的回答思路分享给需要的读者。不管你是准备校招、社招还是已经在带新人都可以拿这个清单来查漏补缺。我不打算写“标准答案复制机”每道题都尽量讲清楚面试官为什么要问、从哪个角度答最容易加分、答完之后还能往哪个方向扩展。建议读者在看题的时候先自己说一遍再对照我的思路这样效果比单纯背题好得多。1. 这份清单为什么值得长期维护面试题的本质是体检1.1 面试题不是题库是对知识体系的扫描Java面试题看起来很多很杂但它们的共同点是每一道高频题背后都藏着一个核心知识点而这个知识点通常不是孤立存在的。比如面试官问HashMap的put流程表面上考集合源码实际上是在考你对哈希算法、扩容机制、链表转红黑树、线程安全隐患这几个知识点的掌握程度。如果你只知道“链表长度超过8转红黑树”这句话却没看过源码那遇到追问“为什么是8不是16”的时候就会卡住。我自己在维护这份清单时发现面试官的设计思路几乎都是“从一道题开始不断往下深挖”。他们想看到的不是你会背概念而是你能围绕一个点展开一个体系。比如从String出发可以延展到字符串常量池、intern方法、不可变类的设计意义再往深就是JVM内存区域里字符串的存放位置。这种层层递进的提问方式就是一次对知识体系完整度的检查。所以我建议读者不要只盯着题目本身而要把每一道题当作一个入口。做笔记的时候给每道题画几个“延伸箭头”把相关的知识点串起来。这份清单里我加了大量扩展点就是为了让读者跟着箭头走形成自己的网状知识结构而不是一条条孤立的记忆线。1.2 我更新这道清单的三条筛选标准并不是所有Java面试题都值得收录。我给自己定了三条硬标准。第一条是必须高频或者说在真实面试中出现次数足够多。那些小概率冷门问题我一般不收除非它能引出特别重要的知识点。第二条是题目必须能带出两个以上的扩展知识纯背结论的题价值太低比如“final关键字能修饰什么”这种虽然基础但扩展有限我就放在基础题列表里不会单独长篇展开。第三条也是最重要的一条答案必须可以被验证。我收的每一题都尽量给出可运行代码、可复现的场景或者可查证的原理。比如线程池拒绝策略我会给出一段触发AbortPolicy的小示例这样读者自己跑一遍就彻底明白了。这个习惯帮了我很大忙因为它逼着我每次整理题目前都去翻源码或者写测试而不是凭感觉写“参考答案”一旦凭感觉很容易写出有误导性的内容。2. Java基础看着简单却最容易翻车的老牌考点2.1 String、StringBuilder与StringBuffer到底怎么答才加分面试第一轮经常从String切入几乎每个Java开发都被问过“String为什么是不可变的”。这里最常见的答法是把String类用final修饰、char数组是final的、每次修改都会生成新对象这几句背出来。这种回答能拿基础分但距离加分还有一段距离。更好的回答是分两步走先讲设计意图再讲底层实现细节。设计意图上String不可变主要有三个考虑。第一个是字符串常量池的复用如果字符串可变那池里“abc”被改成“abd”所有引用它的变量都跟着变了程序逻辑会乱套。第二个是安全性很多地方用String做参数比如类加载器的名字、数据库连接地址如果可变就存在被篡改的风险。第三个是哈希值缓存String的hashCode只用计算一次然后存下来可变对象做不了这个优化。至于StringBuilder和StringBuffer的选择核心点在于线程安全。StringBuffer的方法加了synchronized线程安全但性能差现在很少推荐。StringBuilder不保证线程安全但单线程下性能更好是常规推荐。面试官往往会追问“为什么StringBuilder不安全”这时候可以举例说明多线程同时append时底层数组扩容和值写入可能交替执行导致字符错乱或越界。我在实际项目里几乎只在多线程拼接字符串的场景才考虑用别的方案其他情况一律StringBuilder。2.2 与equals以及hashCode之间的“三角债”这个问题坑过很多人因为它涉及“引用比较”和“值比较”的根本区别。对于基本类型比较的是值本身对于引用类型比较的是内存地址。equals是Object类里的方法默认实现其实也是用比较地址所以如果不重写equals两个内容相同的对象用equals比较结果依然是false。面试官最爱追问的是“为什么重写equals必须重写hashCode”。这需要从HashMap的查找逻辑讲起。HashMap判断两个key是否相同先比较hash值hash相同才用equals确认这就是所谓的“先散列后精确”。如果只重写equals让两个对象相等却没有重写hashCode那它们的hash值可能不同在同一个哈希表里会被分到不同桶位导致你放入的对象永远取不出来。这个坑我在某公司的会员系统里真实遇到过当时用对象做缓存key重写了equals没重写hashCode结果缓存命中率骤降排查了大半天才定位到原因。答题的时候建议顺着这个思路说先解释和equals的区别再说明equals重写的规则最后用HashMap的get和put过程解释hashCode的一致性要求。如果还能顺带提一下“hash冲突”的时候HashMap怎么处理比如链表加红黑树那这一题就能直接串到集合章节面试官会顺着你的话继续问主动权就到手了。2.3 重载和重写别被“静态分派”这几个字吓住重载是在同一个类里方法名相同、参数列表不同它发生在编译期Java通过编译时的静态类型来决定调用哪个方法。重写发生在继承体系中子类重新定义父类的方法方法签名相同运行时会根据对象的实际类型动态分派。很多初级选手以为区别只在于“一个在类内、一个在父子类”这没错但不够深入。加分答法要提到编译期和运行期的差异。重载是按照编译时声明的引用类型来选择方法的所以有“如果父类变量指向子类对象调用重载方法时走的是父类的静态类型”这种反直觉现象。重写则是运行时看堆里实际对象的类型也就是多态的底层机制。如果能顺带讲一讲JVM方法表和方法调用的指令比如invokevirtual、invokestatic这个答案的深度就出来了。我在回答这类问题时会咨询一个简单的例子来强化记忆定义一个父类和一个子类子类里重载一个方法同时重写另一个方法。然后分别用父类引用和子类引用去调用看看哪个走编译期哪个走运行期。自己动手写一遍之后面试时再也不会混淆。3. 集合框架面试官最爱的源码拷问区域3.1 HashMap的put流程、扩容时机和“为什么是8”HashMap是Java面试的必考题没有之一。面试官哪怕不问别的地也一定会碰它。完整的put流程可以概括为先计算key的hash根据hash定位到数组桶位如果桶为空就直接放入如果桶不为空则判断头节点是否相同key相同就覆盖不同就尾插JDK8及以后进入链表或树结构。如果链表长度达到阈值8且数组长度达到64链表会转成红黑树。扩容时机是另一个必问点。默认初始容量16负载因子0.75也就是说元素个数超过12的时候就触发扩容。扩容后数组容量翻倍元素需要重新计算位置。这里要提一个细节扩容不是简单复制而是重新分配桶位因为容量变了元素所在的桶下标会重新计算这也是扩容代价高的原因。关于“为什么阈值是8”这个问题很多参考答案只是搬了源码注释里的泊松分布。建议的回答是分两层第一层是概率上在随机哈希的情况下链表长度达到8的概率极低大约是千万分之六用这个阈值可以在时间和空间上取得平衡第二层是性能上链表查询是O(n)红黑树是O(log n)阈值太低树化频繁代价大阈值太高又失去了优化意义。最后可以补充一点即使达到了8如果数组长度小于64HashMap会优先选择扩容而不是树化因为扩容可以打散链表元素更划算。3.2 ArrayList与LinkedList的复杂度之争不要只会背结论ArrayList底层是动态数组LinkedList底层是双向链表。大多数人都能说出来“数组查询快、增删慢链表增删快、查询慢”这个结论。但面试官在追问之后你会发现结论很多时候是有条件的。比如LinkedList的增删操作只有在指定位置、并且已经拿到了该位置的节点引用时才真正是O(1)。如果你通过add(index, element)在中间插入首先还是得从头遍历找到index位置的节点复杂度其实是O(n)。我见过一个恒生电子的面试官故意让人掉进这个陷阱问到LinkedList插入复杂度时很多候选人脱口而出O(1)完全没有意识到查找过程的存在。ArrayList的增删也同样有隐藏成本。在尾部追加是O(1)但插入中间需要移动后续元素复杂度是O(n)删除元素后数组需要向前移动也是O(n)。所以真实项目中“增删快”并不一定用LinkedList反而因为链表节点占用空间更大、Cache局部性更差在现代电脑上经常不占优势。回答这一题的高阶策略是从底层结构推导复杂度而不是背结论最好还能带一下“实际项目中优先用ArrayList”的使用经验。3.3 ConcurrentHashMap的锁粒度演进从分段锁到CAS加synchronizedConcurrentHashMap也是并发方向的经典题不过很多人只是记结论说“它线程安全”。面试官一旦追问“为什么线程安全”“和Hashtable有什么区别”就需要从历史演进讲起。JDK7时代ConcurrentHashMap采用的是分段锁默认16段每段锁独立所以理论上并发度最高是16。到了JDK8直接颠覆了设计不再有Segment而是用CAS加synchronized来保证线程安全。put操作时如果目标桶为空就用CAS直接放入如果桶不为空就用synchronized锁住头节点再进行插入或更新。为什么改成这种设计可以从两个角度解释。第一是锁粒度更细JDK8的锁只针对单个桶而不是一段区域并发冲突概率更低。第二是synchronized在JDK6之后经历了锁升级优化性能已经很接近甚至优于ReentrantLock没有必要再单独维护一套分段锁结构。回答时可以画龙点睛地提一句从Hashtable全方法加锁到JDK7的分段锁再到JDK8的锁桶整个演进主线就是锁粒度越来越细、并发能力越来越强。4. 并发编程最考验深度也最容易暴露水平的板块4.1 synchronized与ReentrantLock的取舍为什么要选一个“老的”锁并发问题几乎每个Java岗位都问区别只在问多深。最基础的当然是synchronized和ReentrantLock的区别。这是一个需要系统性答案的问题我建议按以下几个维度组织是否可中断、是否支持公平锁、是否支持多个Condition条件、性能对比、语法便利性。简单来讲synchronized是JVM层面的关键字使用简单发生异常时JVM会自动释放锁。JDK6引入偏向锁、轻量级锁、重量级锁的升级过程之后普通场景下性能和ReentrantLock差距已经不大了。ReentrantLock是Java类库提供的功能它需要手动lock和unlock通常配合finally使用。它的优势在于支持响应中断、支持尝试获取锁tryLock、支持公平锁以及可以通过多个Condition实现更灵活的条件等待。我习惯把ReentrantLock的三个独占优势当口诀来记可中断、可超时、可公平。面试里如果听面试官问“什么时候用ReentrantLock”可以结合一个实际场景回答比如需要对锁等待时间做限制的场景或者需要公平排队避免饥饿的场景。只要别说出“synchronized已经过时了”这种话就行面试官更希望听到你理解两者定位的差异而不是非此即彼。4.2 CAS的三大问题ABA、自旋开销和只能保证一个变量CAS是并发编程里的高频底牌面试官问“乐观锁实现原理”时往往就指向它。CAS全称比较并交换底层通过CPU指令实现“比较再更新”的原子操作。Java里的AtomicInteger就是靠它实现的。这一题的加分点在于能讲出CAS的三大问题。第一个是ABA问题值从A变成B再变成ACAS无法感知中间变化解决思路是用版本号Java里的AtomicStampedReference就是干这个的。第二个是自旋带来的CPU开销高并发下CAS反复失败大量线程空转消耗CPU解决思路是自适应自旋、锁升级机制或者用LongAdder这样的分段计数器。第三个是只能保证一个共享变量的原子性多个变量时CAS无法直接用需要锁或者封装成对象再通过AtomicReference来操作。如果能结合自己项目里用AtomicInteger做并发计数的例子来讲会让人觉得你有真实调优经验。我之前在一个秒杀系统的库存扣减里就用过CAS方案后来发现单一热点变量下CAS自旋比synchronized还差就把思路改成了分段计数器加定时合并这个问题让我对CAS的适用边界有了更清晰的认知。4.3 线程池参数七个参数背后的设计逻辑Java线程池的七个参数几乎是年年必考面试官甚至会直接让你现场算“核心线程数怎么设置合适”。与其去背公式不如理解每个参数存在的意义。七个参数分别是核心线程数、最大线程数、空闲线程存活时间、存活时间单位、工作队列、线程工厂、拒绝策略。重点记忆的链路是当提交任务时如果当前线程数小于核心线程数创建新线程执行如果大于核心线程数任务进入队列如果队列满了且线程数小于最大线程数创建临时线程如果队列满了且线程数达到最大线程数触发拒绝策略。这个流程几乎每个架构师面试都会考建议一定要默背并画出来。关于核心线程数的设置比较靠谱的思路是分场景。CPU密集型任务核心线程数建议设为CPU核数加一这样可以降低线程切换开销IO密集型任务因为大量时间在等待IO可以设置到CPU核数的两倍甚至更多。不过公式只是参考真实项目里最好还是先压测再调整。拒绝策略也是高频追问ThreadPoolExecutor内置了AbortException抛异常、Discard静默丢弃、DiscardOldest丢弃队列最老任务、CallerRuns调用者自己执行四种实际场景中我通常选CallerRuns因为它能让任务不丢同时借助调用线程执行形成天然限流。4.4 volatile的可见性和有序性从双重检查锁理解它volatile是并发高频题里的稳定钉子户。两个核心语义是可见性和有序性。可见性指volatile修饰的变量被一个线程修改后会立即刷新到主内存其他线程读到的都是最新值。有序性指编译器或CPU不能把volatile变量相关的指令重排序到读写前后之外。讲解这两个语义最经典的场景是单例模式的双重检查锁。第一层判空是为了避免不必要的加锁第二层判空是为了防止多个线程同时创建实例。但如果不加volatile修饰即使在synchronized块内部创建对象也可能出问题。原因是“new Singleton()”这一步在JVM层面不是原子操作它包含了分配内存、初始化对象、把引用指向内存三个步骤其中第2步和第3步可能被重排。如果线程A先完成了第3步但没完成第2步线程B进来判空发现引用不为空直接拿去用就会拿到一个尚未初始化完成的对象。这一类解释问到细节时最容易得分。你会看到面试官点头因为大多数候选人只背“volatile保证可见性和有序性”这句话说不出具体场景。顺带再提一句volatile不保证原子性比如i这种复合操作还是需要Atomic类或锁这一题的完整度就很高了。5. JVM内存布局、垃圾回收和一次线上Full GC调优5.1 运行时数据区六个区域里最容易混淆的两个JVM运行时数据区几乎是必考基础。堆、虚拟机栈、本地方法栈、方法区、程序计数器、常量池每个区域干什么、哪里会溢出这些问题不难但需要系统性组织。我推荐按“线程共享与线程私有”作为分类主线来讲。堆和方法区是所有线程共享的虚拟机栈、本地方法栈、程序计数器是线程私有的。最容易混淆的是方法区和堆的关系以及常量池到底放在哪里。JDK8之前字符串常量池在方法区里的永久代JDK8之后永久代被元空间取代字符串常量池被移到了堆中。这直接影响你之前了解到的String.intern的返回值判断也会引出一个经典问法“new一个字符串和直接赋值字符串的区别”。另一个容易混淆的点是虚拟机栈和本地方法栈前者服务Java方法后者服务native方法HotSpot虚拟机把它们合并成一个了。我的记忆方法是像回忆自己居住的小区一样堆是公共活动广场所有人都能去栈是每户的私人房间线程各进各的屋方法区是小区公告栏存类型信息和常量。这样解释虽然不够精确但面试中用来先回答“大概”再补充细节就能给面试官留下“理解到位”的印象。5.2 垃圾回收对象怎么判断死亡又有哪些常用采集器判断对象是否存活的标准是可达性分析而不是简单的引用计数。从GC Roots出发沿着引用链往下找能到达的对象就是存活的否则可以被回收。常见的GC Roots包括虚拟机栈中引用的对象、方法区中静态属性引用的对象、常量引用的对象、JNI引用的对象等等。这块答起来相对固定背清楚主流观点就可以了但要注意别把应用计数说成主流因为循环引用问题就可以直接反驳掉引用计数法。垃圾收集器方面重点关注G1。G1的特点是分区收集、可预测停顿它将堆划分为多个大小相等的Region通过维护每个Region的回收价值和回收成本来决定优先回收哪些Region。这才是G1名字的“Garbage First”的由来。回答时可以对比CMSCMS的两个著名缺点是内存碎片化以及并发阶段资源消耗高G1通过整理算法避免了碎片同时可以设置预期的停顿时间。如果面试官问到ZGC可以简要提一下它通过染色指针和读屏障实现了几乎不影响吞吐的极短停顿不过需要小心别被追问得太深。我的经验是聊垃圾回收器时不要变成背诵各个收集器的参数表而要把重心放在“如何根据业务选择合适收集器”的思路上面试官更想听到你具备选型能力。5.3 一次线上Full GC调优的真实排障过程我在某次线上压测时遇到过Full GC频繁触发的问题这个案例我经常在复盘时用到。现象是系统运行十分钟左右接口RT突然飙升日志里大量出现GC日志Young GC后老年代占用快速上升最终触发Full GC。当时我没有直接去调整堆大小而是先抓堆dump和GC日志发现在老年代里大量存活对象是同一个业务缓存对象而且这个缓存对象没有设置过期时间还不断被新请求写入导致老年代逐渐被占满。这类问题的定位思路比具体参数更重要。第一步是看GC日志明确是Young GC频繁还是Full GC频繁第二步是抓堆dump找到占用内存最大的对象类型第三步是回到代码里查看这些对象是谁创建的、为什么没被回收。最终我们把这个缓存的写入频率降下来给缓存加上了过期时间又调整了新生代和老年代的比例Full GC频率从每分钟几次降到几小时一次。在面试中讲这种亲身经历是不管多有效的八股文都比不上。哪怕你的经历很普通只要是真实踩坑后总结出来的面试官都能感觉到。6. Spring核心IoC容器、AOP和事务那些不遇坑不知深的点6.1 Bean的生命周期与循环依赖三级缓存里到底缓存了什么Spring是Java后端面试的半壁江山而Bean生命周期又是Spring的半壁江山。完整生命周期包括实例化、属性填充、初始化、使用、销毁几个阶段其中穿插着BeanPostProcessor的各个扩展点。能说清楚这些基本可以证明你对Spring容器有真理解。循环依赖是常考的深水区。Spring解决单例setter注入循环依赖靠的是三级缓存。第一级缓存里放的是已经完成初始化的成品Bean第二级缓存里放的是提前暴露的早期Bean已经实例化且完成属性填充但还没执行初始化方法第三级缓存里放的是ObjectFactory也就是一个能生成代理对象的工厂。三级缓存的必要性在于如果Bean需要AOP代理第三级的ObjectFactory可以在提前暴露时生成代理对象从而保证最终注入的和实际使用的是同一个代理。回答循环依赖的关键是区分构造器注入和setter注入。构造器注入天然无法解决循环依赖因为Bean在创建时需要先有依赖对象这在一个相互依赖的构造过程中会直接报错。而setter注入可以在实例化之后再填充属性所以才能利用三级缓存提前暴露半成品。理解了这一点你就能解释为什么Spring官方推荐构造器注入不是因为循环依赖而是因为它保证了依赖的不可变更性。6.2 AOP代理失效为什么自调用走不到拦截逻辑AOP的面试题通常集中在“哪些情况下AOP会失效”。最常见的场景是同一个类里的方法自调用。比如一个类里方法A调用方法BB上标了TransactionalA没有标此时B的事务是不会生效的。原因是AOP基于代理外部调用进入的是代理对象事务通过代理对象的方法拦截来实现但A方法在执行时调用B方法用的是this调用this指向的是原始对象而不是代理对象自然绕过拦截器。类似的原因也能解释为什么private方法上标注的Transactional不生效因为代理对象无法增强private方法。还有一种是类没有被Spring管理比如new出来的对象调方法那更不可能有AOP生效。最后的总结建议把“AOP生效的前提是方法经由代理对象调用”这句话当提纲面试官喜欢听到你对本质的概括而不是一个个孤立场景的罗列。6.3 Transactional事务失效的五个典型场景事务失效的坑几乎是必考清单而且出题人很喜欢把场景做成选择题“以下哪个方法事务会失效为什么”。我总结过一份高频失效原因清单按出现频率排列方法内部自调用导致代理失效异常被try-catch吞掉事务感知不到抛出的是检查异常而事务默认只对RuntimeException回滚方法被非public修饰数据库引擎不支持事务比如MyISAM其中“异常被吞”和“检查异常不回滚”是最容易混淆的。默认情况下Spring只对RuntimeException和Error回滚如果你故意捕获了异常事务就无法知道发生了什么自然不会回滚。想说服面试官你理解了事务的底层机制可以说Spring事务本质是AOP切面它捕捉到方法抛出异常才执行回滚逻辑如果异常根本没有传播出来切面就认为方法执行成功。这么一解释前面的五个场景全都串起来了。7. 数据库索引、事务隔离和一条慢SQL的排查链路7.1 为什么索引底层用B树而非其他树数据库索引是Java后端面试和实际开发的交汇点。最常问的是“为什么MySQL InnoDB索引用B树而不用二叉树、哈希表、B树”。这个问题有一个很好的回答框架从每种数据结构的特性出发分析它为什么不适合。哈希表等值查询最快但做不了范围查询所以直接排除。二叉搜索树极端情况下会退化成了链表同时也无法支撑千万级数据的存储树太深会导致磁盘IO过多。红黑树虽然平衡但同样存在树深问题不适合磁盘这种每次访问都有固定代价的介质。B树每个节点可以存储多个关键字降低了树高但它的数据分散在每个节点上做范围查询时中序遍历复杂。B树把所有数据存在叶子节点叶子节点之间通过指针串联成链表这样范围查询只需要遍历链表加上非叶子节点只存索引不存数据一个节点能放更多索引树高进一步降低磁盘IO次数更少。回答的时候能把这个推导过程讲明白比直接背“B树更合适”要有说服力得多。如果还可以补充聚簇索引和非聚簇索引的区别以及回表概念这题基本就满分了。7.2 事务隔离级别与MVCC的关联事务隔离级别包括读未提交、读已提交、可重复读、串行化。从名词解释到存在的问题这一块是数据库面试的常规必考。读未提交会有脏读读已提交解决了脏读但会有不可重复读可重复读解决了不可重复读但可能还有幻读串行化可以杜绝所有问题但性能极差。MySQL InnoDB默认隔离级别是可重复读但它实际上也基本解决了幻读。解决方案有两种一是MVCC多版本并发控制利用隐藏字段和undo日志生成快照在普通读场景下事务读取的是快照版本不会被其他事务新插入的数据影响二是当前读场景下比如select for update会通过间隙锁把范围内的所有间隙锁住防止其他事务在范围内插入数据。把这两个机制讲清楚面试官就会觉得你真的理解隔离级别而不是停留在数据库教科书层面。7.3 一条慢SQL是这么被排查出来的面试题经常给一个场景某个接口突然很慢数据库CPU升高你怎么排查。这题考察的是排障思路。我的固定套路是先查慢查询日志找到具体SQL然后执行explain重点看几个字段type是否达到range或ref而不是ALL全表扫描、key是否实际用到索引、rows扫描行数是否巨大、Extra里有没有Using temporary和Using filesort。常见索引失效情况需要背熟比如对索引列使用函数或计算、隐式类型转换、最左前缀原则被破坏、like以%开头、or条件中有一个字段无索引。这里有一个我重复踩过很多次的坑明明where条件里有索引但字段类型不一致比如字符串字段传了数字参数MySQL可能做隐式转换导致索引失效。排查时不要光看SQL结果对不对还要注意类型匹配。回想一次优化某报表查询的经验一个耗时3秒的查询分析后发现是两张大表join连接字段上的字符集不一致导致索引失效修复之后秒回。这类经验在面试里说出来比背十条索引优化口诀都更能体现实操能力。8. 分布式场景缓存、锁和消息队列的必问题8.1 缓存穿透、击穿、雪崩三个“灾难”如何应对分布式面试里缓存的三兄弟几乎必提穿透、击穿、雪崩。穿透指的是查询一个根本不存在的数据缓存和数据库都没有每次请求直接打到数据库。解决思路有布隆过滤器前置拦截以及缓存空值并设置短过期时间。建议优先用布隆过滤器因为它不需要存储空值数据内存占用小缺点是可能有误判所以还需在数据库层做好兜底。击穿指的是热点key过期的一瞬间大量并发请求同时打到数据库。常见解决方案是互斥锁和逻辑过期。互斥锁方案下只有一个线程去数据库加载数据并写缓存其余线程等待逻辑过期方案则不设置物理过期时间而是存一个逻辑过期时间后台异步刷新。两者各有取舍互斥锁简单但会短暂阻塞逻辑过期不阻塞但实现复杂。雪崩指大量key同时失效或者Redis节点故障导致数据库负载暴增。解决思路主要分两块一是让key过期时间带随机值避免同一时间集体失效二是对Redis做高可用部署加哨兵或集群。如果面试官追问“Redis挂了怎么办”可以从本地缓存兜底、请求限流和熔断器三个层面回答。8.2 分布式锁Redis与ZooKeeper方案怎么权衡分布式锁考察的是你对CAP和一致性的理解。最基础方案是Redis的setnx加过期时间但要注意几个坑一是必须保证setnx和expire是原子操作否则锁没设置过期时间就宕机其他线程永远拿不到锁二是释放锁时要验证是不是自己加的锁不能把别人刚持有的锁误删了。Redisson这个框架通过看门狗机制自动续期就是为了避免业务还没执行完锁就过期的问题。另一种方案是ZooKeeper的临时顺序节点通过创建节点和监听机制实现锁。临时节点的好处是客户端会话断开节点会自动删除不会出现锁永久不释放的问题这比Redis的过期时间更优雅。但ZooKeeper方案吞吐量不如Redis并且每次加锁都有创建节点和监听的网络开销性能会低于Redis方案。面试官问到选型对比时我给出的建议是高吞吐场景用Redis强一致和可靠性要求极高的场景用ZooKeeper。如果还有延展空间可以提一下Redlock算法但顺手点一句“业界对Redlock的争议也不小”就好不用展开太多。8.3 消息队列的可靠性投递从生产到消费的完整链路以RabbitMQ或Kafka为例消息可靠性问题可以拆成三段来答。生产端要确保消息不丢常见方案是发送方确认模式比如RabbitMQ的ConfirmCallback或者Kafka的acksall加重试机制。Broker端要确保消息不丢一方面靠持久化另一方面靠副本机制Kafka的多副本和ISR机制就是为这个设计的。消费端要保证不丢关键是消费完成后才提交offset同时关闭自动提交避免消息还没处理完就提交了位点。但最容易被忽略的是“不丢”的另一半是“不重复”。即使我们做到了不丢重试和网络抖动还是会带来重复消息所以消费端必须幂等。设计幂等的手段包括在消费逻辑里利用数据库唯一键、Redis分布式锁或者维护一个消费去重表。完整的可靠性方案从来不是靠单点而是靠协议保证加业务幂等共同兜底。我见过太多只聊“手动ack”就敢说自己懂消息队列的候选人一问“重复消息怎么办”就卡壳。所以这道题一定要观点完整最好表达“生产端不丢、消费端不重、幂等兜底”三个层次这会让面试官对你的系统设计能力刮目相看。9. 怎样把这份清单用出最大价值先别急着从第一题背到最后一题那样不仅容易忘还容易把思路背死。我推荐的做法是按模块来每个礼拜深攻一个主题比如这一周专门整理集合框架那么就把HashMap、ConcurrentHashMap、ArrayList、LinkedList放到一起复习顺便看一遍源码。面试其实不怎么考“你背了多少题”它考的是“你会不会像程序员一样思考”而思考能力只能通过反复推演和写demo来提升。每道题看完之后拿出一张白纸假装面前坐着面试官把自己准备讲的思路说一遍。这一步被称为“费曼式学习”它的目的是逼着你组织语言当你发现说出口的句子断断续续说明这个知识点还没真正变成你的。我在准备面试最紧张的时候每天都对着电脑屏幕开录音然后回放自己的回答这个过程听着有点傻但效果极其明显。最后分享一个我踩过的坑不要沉溺于“看了很多面经”的假象。面经的作用是帮你识别考点而不是代替思考。真正让面试官记住你的是在某个问题下面的深度追问里你能比他更快地讲出底层细节和你的实战取舍。这份Java面试题清单会一直更新下去下一次准备跳槽的人希望你能从这些题目里看到的不只是“答案”而是背后的知识网络。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表