ARTICLE DETAIL

资讯详情

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

Java面试核心模块攻略:集合并发JVM框架一次讲透

Java面试核心模块攻略:集合并发JVM框架一次讲透 最近帮几个准备跳槽的朋友做了几轮Java模拟面试发现一个共同的问题大家背了不少题八股文张口就来但一旦被追问“为什么这样设计”“你线上遇到这种情况怎么处理”很多人就开始卡壳。这其实不怪大家而是准备方式出了问题——只记住了结论没有理解结论背后的推理链路。这篇攻略我按Java面试最核心的五个模块来写基础、集合、并发、JVM、框架。每个模块我都不会只罗列题目和答案而是把面试官真正想考察的点、你该怎么组织回答、哪些地方容易被人追问到死角都梳理清楚。无论你是准备校招还是社招二三年经验还是五年以上都能在这套体系里找到自己该重点突破的方向。1. 先把面试准备这件事想清楚五模块的学习路径规划很多人的复习顺序是拿到一本面试题从头背到尾背完一个知识点马上背下一个。这其实很低效。Java面试的知识点之间是有依赖关系的你连JVM内存模型都没搞明白就去背G1收集器的参数只能死记硬背换个问法就懵。我建议按照基础到框架的主线来推进先搭骨架再填血肉。1.1 不同职级面试的考察重心差异先别急着看题先搞明白你目标岗位的面试官在找什么样的人。如果是校招或者两年以内的初级岗面试官重点考察的是基本功扎不扎实集合、并发、JVM的基本概念能不能讲清楚代码规范性和基础算法能不能过关。这个阶段问的就是Common知识点的标准答案比如HashMap的put流程、synchronized和ReentrantLock的区别这类。到了三年以上经验的岗位面试官基本不会再问标准答案了。他们会把题目包装成一个实际场景线上接口突然变慢你怎么排查、数据库和缓存一致性问题怎么解决、一个高并发场景下你会怎么设计。这个时候你光会背JVM参数没有用你得能说出完整的排查链路能结合自己的项目经验讲出权衡和取舍。五年以上的高级岗位考察重心又不一样。面试官更关注你对技术原理的深度理解、对系统架构的把控能力以及是否具备技术决策能力。比如问到线程池不会只问参数有哪些而是让你结合业务场景设计一套线程池方案讲讲为什么这么配置、怎么监控和动态调整。所以准备面试的第一步不是打开题库而是先定位自己我现在是什么水平我目标岗位需要什么水平中间的差距在哪里。1.2 高频考点分布先抓大放小再查漏补缺以我观察到的实际面试情况来看Java基础部分的高频考点集中在String相关的底层机制、集合框架中的HashMap和ArrayList、并发中的synchronized锁升级和线程池、JVM中的内存区域划分和垃圾回收算法。这四个方向占了Java基础面试题量的七成以上。框架部分的重点则集中在Spring的IoC和AOP原理、SpringBoot的自动配置机制。而像类加载器、JIT编译这类冷门考点除非你面的是中间件团队或者JVM专项岗位否则很少出现在常规面试里。我的建议是复习时按“核心必考、重点掌握、了解即可”三个梯度来分配精力。第一梯队必须能讲出源码级别的细节第二梯队要能说出核心原理和设计思路第三梯队只需要知道概念和适用场景。这样能保证在有限的时间内把投入产出比最大化。2. Java基础那些“感觉会了一追问就露馅”的经典考点Java基础是整个面试的地基但恰恰是最容易被轻视的部分。很多候选人觉得基础简单结果在String、equals、hashCode这类题目上翻了车。原因在于这些知识点的表象太简单了简单到你以为自己懂了但面试官只需要换个角度问一下“为什么”就暴露了理解深度。2.1 String与字符串常量池一个知识点能问出三个层次String相关的题目是Java面试必考题几乎没人能躲过。最经典的问法是“new String(abc)一共创建了几个对象”这道题看起来是个脑筋急转弯其实考察的是JVM内存模型中字符串常量池和堆的关系。这道题有三个层次。第一层是概念层你得知道Java中有字符串常量池直接写双引号声明的字符串会放进常量池而new出来的String对象会存放在堆中。所以new String(abc)如果常量池中还没有abc这个字符串会创建两个对象一个在常量池一个在堆中。如果常量池已有abc那就只创建一个堆对象。第二层是机制层面试官会追问字符串常量池在JDK不同版本中存放位置的变化。JDK 1.7之前字符串常量池存放在永久代JDK 1.7及之后移到了堆中。这个变化的原因是永久代空间有限字符串大量使用容易触发OutOfMemoryError移到堆后可以由GC统一管理。第三层是应用层面试官会问String的intern()方法的作用和使用场景。intern()方法会尝试把当前字符串放入常量池如果常量池中已有相同内容的字符串就返回常量池中的引用。在实际项目中可以用来减少重复字符串占用的内存但要注意如果大量调用intern()反而可能增加常量池的压力需要结合业务场景评估。2.2 equals、hashCode与包装类缓存的连锁考点聊完String面试官很容易顺藤摸瓜问到equals和hashCode的关系。这是Java基础中最高频的追问组合之一尤其是“为什么重写equals必须重写hashCode”这个问题。这里不能只背结论要讲清楚hashCode的用途它决定了对象在HashMap、HashSet这类散列集合中的存储位置。如果两个对象equals相等但hashCode不同它们在HashMap中会被散列到不同的桶就会导致同一个key在集合中出现两份数据破坏了集合的语义。再往下挖就是包装类的缓存机制。Integer缓存了-128到127之间的数值在这个范围内Integer用比较结果相等超出这个范围就不相等。这个知识点常和自动装箱拆箱一起考很多人在这道题上吃亏是因为不知道Integer.valueOf()内部有缓存判断逻辑。我建议你在准备这个知识点时可以顺手把Integer、Long、Character等包装类各自的缓存范围都过一遍。这样面试官问任何一个都能从容应对还能顺势展示你对源码的熟悉程度。2.3 面向对象与异常把概念题答出设计感面向对象的基础概念考察频率很高但这类题想答好反而更难因为概念太基础了大家都知道你需要做的是在答案里体现设计思维。比如抽象类和接口的区别只回答“抽象类可以有构造方法接口不能”这种列差异式的答案是拿不到高分的。更好的答法是先讲概念再讲演进最后落到设计场景。接口在Java 8之后支持默认方法和静态方法Java 9之后还支持私有方法接口和抽象类的边界越来越模糊。面试官其实想听的回答是在一个具体业务场景中你为什么会选择抽象类而不是接口或者反过来。比如定义一个动物基类用抽象类定义一组能力规范用接口。关键是说出选型背后的设计思考。异常体系也是常考的基础点。受检异常和非受检异常的区别要能说清楚Error和Exception的关系要能讲明白。面试官还会追问项目中如何自定义异常、如何设计全局异常处理。这里可以结合Spring的ControllerAdvice和ExceptionHandler来回答把基础知识和框架实践串起来会显得你的知识体系非常完整。3. 集合框架从“会用”到“能讲源码”的分水岭集合框架是Java面试的重灾区也是区分候选人是“背答案型”还是“源码阅读型”的关键模块。其中HashMap又是重灾区中的重灾区可以说十场Java面试九场问HashMap。这一章我把HashMap讲透同时把ArrayList、LinkedList以及并发集合分开来讲每个都有对应的答题要点。3.1 HashMap的七问连环炮把put流程说到源码级别最高频的一个面试题是“说一下HashMap的put流程”。这道题表面是考源码实际是考你对数据结构、散列函数、扩容机制的综合理解。我的建议是你至少要能分七步回答到源码级别。第一步计算key的hash值。这里要提一下扰动函数JDK 1.8中HashMap对key的hashCode做了高16位与低16位的异或运算目的是让高位参与低位运算降低哈希冲突概率。第二步数组为空时触发resize()初始化默认容量是16。第三步用(n-1)hash计算数组下标这里用位运算而不是取模性能更高前提是数组长度是2的幂次方。第四步如果该位置为空直接newNode插入。第五步如果该位置不为空判断key是否相等相等则覆盖value。第六步如果不相等判断当前节点是否是树节点是红黑树就走红黑树插入逻辑否则遍历链表遍历过程中如果发现链表长度达到8且数组长度达到64链表转红黑树。第七步插入完成后检查size是否超过threshold超过则resize扩容为原来的两倍。回答完这套流程面试官大概率会追问“为什么链表转红黑树的阈值是8”。这个问题是有讲究的来源于泊松分布的概率计算。在负载因子0.75、哈希函数随机分布的前提下链表长度达到8的概率已经极其微小所以选择8作为阈值既保证了红黑树的优势能在极端情况下发挥作用又避免了红黑树节点占用空间较大带来的浪费。再往上追问就是JDK 1.7和1.8的差异核心差异有三个数据结构从数组加链表变成数组加链表加红黑树解决哈希冲突的插入方式从头插法变成尾插法扩容后的rehash计算做了优化1.8通过判断节点的hash值与旧数组长度的高位是0还是1来决定新位置是原位置还是原位置加旧容量减少了重新计算哈希的开销。3.2 ArrayList与LinkedList选型不是背结论是算复杂度ArrayList和LinkedList的对比是集合模块的常考题。最常见的背法就是“ArrayList查询快增删慢LinkedList增删快查询慢”。这个背法没错但不严谨面试官只要追问一句“ArrayList在尾部插入也慢吗”很多人就答不上来了。实际上ArrayList在尾部插入元素的平均时间复杂度是O(1)只有在触发扩容时才有额外的复制开销。而LinkedList虽然增删是O(1)但如果你要删除指定位置的元素你需要先遍历到那个位置这个遍历过程是O(n)的。所以在实际业务中绝大多数场景下ArrayList的性能反而更好因为CPU对连续内存的缓存友好度远高于分散的节点。还有一点值得注意LinkedList实现了Deque接口可以用作栈和队列这是它相对ArrayList的一个优势。面试中可以提一句“如果需要频繁在头部插入删除LinkedList比ArrayList更合适”这样会显得你的选型是有数据支撑的而且能区分开“理论上的复杂度”和“实际场景中的复杂度”。3.3 快速失败机制与并发集合从异常名理解设计意图集合这块还有一个高频考点是fail-fast机制。典型问法是“在遍历集合时为什么修改集合会抛出ConcurrentModificationException”。这个问题的答案在ArrayList内部维护的modCount字段每次结构性修改都会将modCount加一迭代器在遍历时用expectedModCount做校验一旦发现两个值不一致就抛出异常。这个机制的设计意图是尽早暴露并发修改问题避免后续出现更难以排查的数据不一致。不过这部分更值得准备的是并发集合的底层实现尤其ConcurrentHashMap是面试热点。JDK 1.7的ConcurrentHashMap使用分段锁实现每个Segment继承自ReentrantLock不同段之间可以并发访问。JDK 1.8则抛弃了分段锁转而使用CAS加synchronized实现并发控制锁粒度从Segment级别细化到数组元素级别锁的竞争概率更低并发度更高。这里我建议你顺便复习一下CopyOnWriteArrayList这个并发容器它在读多写少的场景非常有用。它通过写时复制来保证并发安全读操作不加锁直接读数组写操作先复制一个新数组在新数组上修改然后把volatile修饰的数组引用指向新数组。代价是写操作的内存开销比较大适合配置类数据缓存这类读多写少、数据量不大的场景。3.4 排序与集合类算法冒泡排序这类基础也要能手写面试中手写算法环节偶尔会考到排序尤其是冒泡排序、快排这类基础排序算法。很多人觉得冒泡排序太简单了不值得准备但在压力面试下临时手写还是有不少人写错边界条件。给你一个冒泡排序的参考实现你可以在面试前手敲几遍确保不出错public void bubbleSort(int[] arr) { if (arr null || arr.length 2) { return; } for (int i 0; i arr.length - 1; i) { boolean swapped false; for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped true; } } if (!swapped) { break; } } }这段代码加了一个swapped标记位如果某一轮没有任何交换说明数组已经有序可以提前退出。这个优化虽然简单但在面试中写出来会显得你比单纯背排序模板的人多想了一层。4. 并发编程从synchronized到AQS建立并发的知识网络并发编程是整个Java面试中最难啃的模块也是最有含金量的模块。难点在于并发知识天然是网状结构的synchronized、volatile、CAS、AQS、线程池、ThreadLocal之间互相牵连你单独背哪一个都撑不住追问。我觉得应对并发面试的正确姿势不是逐个击破而是先把整个知识网络搭建起来再不断充实细节。4.1 synchronized锁升级从无锁到重量级的完整链路synchronized在JDK 1.6之后做了大量优化引入了偏向锁、轻量级锁、重量级锁的升级机制。面试官最喜欢问你“synchronized的锁升级过程”这个问题答得好能直接证明你对并发底层机制的理解。锁升级的完整链路是这样的当一个线程首次访问同步代码块时锁对象处于无锁状态JVM会通过CAS操作把锁对象的对象头中的Mark Word设置为偏向当前线程的ID这时就是偏向锁。偏向锁的意思是如果同一个线程再次进入同步代码块就无需再做任何同步操作。当另一个线程尝试竞争偏向锁时偏向锁会被撤销升级为轻量级锁。轻量级锁的实现是线程在自己的栈帧中创建锁记录空间然后通过CAS尝试把对象头中的Mark Word替换为指向锁记录的指针替换成功则持有锁失败则自旋等待。如果自旋超过一定次数或者竞争的线程数过多轻量级锁就会膨胀为重量级锁进入操作系统的互斥量阻塞。这里要重点记住一个关键点偏向锁在JDK 15中已经被废弃。但面试中问到经典的锁升级过程回答的时候可以补充一句“虽然偏向锁在新版本被标记为废弃但理解它的设计思路对理解对象头和锁机制仍很有帮助”这样既展示了知识的新鲜度又不会显得死记硬背。4.2 volatile与CAS并发编程的基石volatile是面试中必问的关键字考察点集中在三个方面可见性、有序性、不保证原子性。可见性指的是一个线程修改了volatile变量的值其他线程能立刻看到。它的实现原理是写volatile变量时会生成一个Lock前缀指令这个指令会强制将当前处理器缓存行的数据写回主内存同时使其他处理器中缓存该变量的缓存行失效。有序性指的是volatile通过内存屏障来禁止指令重排序避免单例模式中常见的“半初始化对象”问题。不保证原子性则是说volatile只能保证读写操作的原子性i这种复合操作依然不是线程安全的。CASCompare And Swap是并发编程的底层基石compareAndSwapInt这类操作由JVM通过Unsafe类直接调用CPU的原子指令实现。CAS的核心是三个值内存地址、期望值、新值只有当内存中的值等于期望值时才把它更新为新值。面试中CAS可以和synchronized做对比CAS是无锁的非阻塞算法适用于并发竞争不激烈的场景竞争激烈时CAS会频繁自旋消耗CPU资源。CAS还有一个经典问题是ABA问题。一个值从A变成B再变回ACAS会误认为它没有变化过。解决方案是使用带版本号的原子类AtomicStampedReference内部维护了版本号每次修改都会更新版本号从而避免ABA问题。4.3 AQS与JUC工具类面试官爱追问的底层设计AQSAbstractQueuedSynchronizer是java.util.concurrent包的灵魂ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier这些常考工具类的底层实现全部基于AQS。如果你能看懂AQS的设计很多并发类的题目都会迎刃而解。AQS的核心是一个volatile int类型的state变量以及一个CLH变体的FIFO等待队列。state表示共享资源的同步状态在ReentrantLock中state表示持有锁的次数可重入一次就加一在Semaphore中state表示剩余的许可数量。线程获取锁失败时会被封装成Node节点加入等待队列尾部通过自旋和LockSupport.park()阻塞自己锁释放时会唤醒队列头部的后继节点。在面试中面对AQS我建议的回答套路是先讲整体设计意图再讲一个具体实现最后落到应用场景。比如面试官问“ReentrantLock和synchronized的区别”你可以这样说synchronized是JVM层面的隐式锁使用简单但功能受限ReentrantLock是JDK层面基于AQS实现的显式锁支持公平锁和非公平锁、支持中断响应、支持超时等待、可以绑定多个Condition条件队列。这样的回答既有层次又体现了底层知识。4.4 线程池参数配置与生产环境的血泪教训线程池是并发模块的高频考点几乎每个面试官都会问。最基础的问题是七大参数核心线程数、最大线程数、空闲存活时间、时间单位、任务队列、线程工厂、拒绝策略。你不仅要能背参数还要能结合场景给出配置建议。关于核心线程数的设置没有标准答案要根据任务类型区分。CPU密集型任务核心线程数设置为CPU核数加一即可因为CPU密集型任务主要消耗CPU资源线程太多反而增加上下文切换开销。IO密集型任务核心线程数可以设置得更大比如CPU核数的两倍因为IO操作期间线程会阻塞更多线程可以充分利用等待时间。关于任务队列和执行顺序阿里开发规范中有一个建议避免使用无界队列因为无界队列在任务堆积时会导致内存耗尽。推荐使用有界队列并配合合理的拒绝策略。这里再补充一个更细的考点线程池的执行顺序是当线程数小于核心线程数时创建新线程执行任务当线程数达到核心线程数时任务进入队列当队列满且线程数小于最大线程数时创建新线程执行任务当队列满且线程数达到最大线程数时触发拒绝策略。这个顺序很多人理解反了误以为先创建线程到最大线程数再进队列面试时一定要说清楚。四种拒绝策略也要会区分AbortPolicy直接抛出RejectedExecutionExceptionCallerRunsPolicy由提交任务的线程自己执行任务DiscardPolicy静默丢弃任务DiscardOldestPolicy丢弃队列中最旧的任务。生产环境最常用的是CallerRunsPolicy因为它不会丢失任务同时被拒绝的任务由调用线程执行还能起到天然限流的作用。4.5 ThreadLocal使用场景和内存泄漏隐患ThreadLocal在面试中的出镜率也很高尤其是和内存泄漏结合起来的问法。ThreadLocal的原理是每个Thread内部有一个ThreadLocalMapThreadLocal作为key线程私有的变量作为value。所以不同线程之间通过ThreadLocal存取的数据互不干扰常用于传递用户上下文、数据库连接、事务信息等场景。面试官最爱追问的问题是“ThreadLocal为什么会导致内存泄漏”。原因在于ThreadLocalMap中的Entry继承了WeakReferencekey也就是ThreadLocal对象是弱引用一旦外部没有强引用指向ThreadLocal对象GC回收时key就会被回收但value仍然被ThreadLocalMap中的Entry强引用着如果线程一直存活value就永远无法被回收形成内存泄漏。解决方案也很简单使用完ThreadLocal后调用remove()方法主动清理。在web应用中如果使用了ThreadLocal传递用户信息务必在请求结束的过滤器或拦截器中调用remove()否则使用线程池时复用的线程会把上一个请求的数据传递给下一个请求造成严重的数据串线问题。5. JVM内存模型、垃圾回收与线上问题排查JVM模块是Java面试的压轴重头戏。这部分知识偏底层很多人觉得抽象但实际上JVM的知识点是非常体系化的。从内存模型到垃圾回收再到G1收集器再到线上排查是一条逻辑紧密的主线。5.1 从JDK/JRE/JVM的关系说起内存区域划分先说一个热身题JDK、JRE、JVM三者的关系。JVM是Java虚拟机负责运行字节码文件JRE在JVM的基础上增加了Java类库是Java程序的运行环境JDK在JRE的基础上增加了编译器和开发工具是Java开发工具包。三者是层层包含的关系这点必须搞清楚因为它直接关系到你对Java程序从编译到运行的完整流程的理解。进入正式考点JVM内存区域划分是必考题。JVM的运行时数据区分为线程共享和线程私有两部分。线程共享的区域包括堆和方法区线程私有的区域包括虚拟机栈、本地方法栈和程序计数器。在JDK 8及之后的版本中方法区被元空间取代元空间使用本地内存不再受JVM堆内存大小限制默认情况下它的大小只受操作系统可用内存限制。这个变化的背景是永久代经常因为动态生成大量类而内存溢出改用元空间后类的元数据存储在本地内存中OutOfMemoryError的概率大大降低。堆内存的划分也要掌握新生代Eden区、两个Survivor区和老年代。默认比例是Eden占8份两个Survivor各占1份也就是811。对象优先在Eden区分配经过一次Minor GC后存活的对象进入Survivor区年龄加1当年龄达到默认的15时进入老年代。5.2 垃圾回收算法与分代收集垃圾回收算法的核心是判断对象是否存活面试中至少要能回答两种判断方式引用计数法和可达性分析。引用计数法的缺点是难以解决循环引用问题所以主流的JVM使用的是可达性分析从GC Roots出发沿着引用链遍历能到达的对象即存活对象。GC Roots包括哪些对象也要能列举虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象以及Java虚拟机内部的引用比如基本数据类型对应的Class对象、常驻的异常对象、系统类加载器等。面试中常考的垃圾收集算法有三个标记-清除、标记-复制、标记-整理。三种算法各有优劣现代的垃圾收集器基本都是组合使用。新生代对象存活率低适合用复制算法老年代对象存活率高适合用标记-清除或标记-整理。常见的垃圾收集器包括Serial、ParNew、Parallel Scavenge、CMS、G1新生代和老年代各有不同的组合方式。面试中问得最多的是CMS和G1的对比。CMS是基于标记-清除算法实现的目标是获取最短回收停顿时间它的缺点是会产生内存碎片、无法处理浮动垃圾。G1则把堆划分成多个大小相等的Region能对部分Region进行回收做到了可预测的停顿时间。5.3 G1收集器为什么它能做到可预测停顿关于G1收集器搜索热词里也有“jvm g1收集器”说明这是面试中的高频追问点。G1的设计核心是把整个堆划分成一个个大小相等的Region每个Region都可以独立扮演Eden、Survivor或者Old区。这种设计彻底打破了物理内存上新生代和老年代的连续划分为整体区域收集提供了基础。G1的垃圾回收过程分为四个阶段Young GC、并发标记、混合回收、必要时Full GC。Young GC阶段回收所有新生代Region并把存活对象移动到Survivor区或晋升到老年代。并发标记阶段会找出所有需要回收的Region。混合回收阶段回收部分老年代Region和新生代Region目标是控制停顿时间在指定范围内。G1通过维护一个优先级列表优先回收垃圾最多的Region从而把回收成本控制在用户可接受的停顿时间内。RSetRemembered Set是G1的核心数据结构它记录了其他Region中的对象对当前Region中对象的引用关系。有了RSetG1在回收一个Region时不需要扫描整个堆就能知道哪些外部对象引用了当前Region极大减少了扫描范围。面试时如果能讲到这个程度面试官基本会认可你对G1的理解。如果再追问调优参数你需要能说出-XX:MaxGCPauseMillis的作用——它是G1的目标停顿时间参数默认值是200毫秒G1会根据这个目标自动调整新生代的大小和各阶段的回收策略。5.4 JVM调参与常见OOM排查套路JVM调优不是面试中的必考环节但“线上OOM怎么排查”是非常高频的考察题目。这类题没有标准答案但是有标准套路我建议你按以下链路来组织回答。第一步先确认发生了OOM通过日志查看异常信息。引入JVM参数-XX:HeapDumpOnOutOfMemoryError让JVM在OOM时自动导出堆转储文件。注意一下如果应用是部署在Docker容器里的需要确保JVM能读取到容器内的cgroup内存限制否则可能配置的堆内存大小超出了容器可用内存导致JVM启动直接被系统杀掉此时日志里不会有OOM堆栈需要查看宿主机层面的OOM Killer日志。第二步拿到堆转储文件后用MAT或JVisualVM分析确认是堆内存泄漏还是内存溢出。如果是内存泄漏重点查看对象的GC Roots引用链找到泄漏点如果是内存溢出说明堆内存确实不够需要调整-Xmx参数或者优化业务逻辑。第三步结合代码排查是否有什么静态集合类一直往里放数据、是否有ThreadLocal没有清理、是否有数据库连接或IO流没有关闭。这些问题都是生产环境最常见的OOM诱因。有一点我特别想提醒你面试中如果能把“查看GC日志、导出堆转储、用MAT分析”这条排查链路讲完整比单纯背JVM参数更能打动面试官因为这说明你有线上问题处理的经验而不只是看了几篇调优文章。6. 主流框架Spring与SpringBoot面试官问的其实是框架思维框架部分的面试题近几年越来越偏向原理。以前问“Autowired和Resource的区别”就够用了现在面试官更爱问“Spring是如何解决循环依赖的”“SpringBoot的自动配置是怎么实现的”。这说明行业对候选人的要求已经从“会用框架”提升到了“理解框架设计思想”的层面。6.1 IoC与AOP用“容器管理对象”的视角理解SpringSpring的IoC控制反转和AOP面向切面编程是框架面试中最基础也最核心的内容。IoC的理解关键在“反转”二字传统开发中对象由使用者自行new出来依赖关系由使用者自己维护Spring框架中对象的创建和生命周期管理全部交给容器使用者只需要声明依赖容器负责注入。这种设计的好处是对象之间的耦合度降低代码的可测试性和可维护性大幅提升。AOP则是对OOP的有力补充解决的是日志、事务、权限校验这类横切逻辑的重复代码问题。AOP的底层实现是动态代理Spring中如果目标类实现了接口默认使用JDK动态代理如果目标类没有实现接口则使用CGLIB代理通过生成目标类的子类来增强方法。需要留心的是Spring Boot 2.x之后默认使用CGLIB代理即使目标类实现了接口也不再使用JDK动态代理。这个变化导致了一些兼容性问题比如Transactional自调用失效、Configuration类内部方法调用导致AOP失效等这些细节在面试中可以作为加分项主动提出来。6.2 Bean生命周期与Spring事务Spring的Bean生命周期是框架面试的高频题问法通常是“说一下Spring Bean的生命周期”。这个问题我建议你抓住三个阶段来记忆实例化、初始化、销毁。实例化阶段Spring通过构造器或工厂方法创建Bean实例然后进行属性填充也就是依赖注入。之后Bean会经过一系列的BeanPostProcessor处理包括检查是否实现了各种Aware接口比如BeanNameAware、ApplicationContextAware调用对应的回调方法。接着还有InitializingBean的afterPropertiesSet()方法以及自定义的init-method方法。销毁阶段则是容器关闭时调用DisposableBean的destroy()方法和自定义的destroy-method方法。这个流程可以用一句话概括实例化、属性填充、初始化回调、使用、销毁回调。能把这个流程背下来的人不少能结合BeanPostProcessor的扩展点来谈自己怎么在项目里使用的人不多后者才是面试官想听的。Spring事务也是必考点。核心考点包括事务的传播行为、隔离级别以及事务失效的场景。关于事务失效我举几个常见的例子方法不是public的、类没有被Spring管理、自调用绕过代理、异常被捕获没有抛出、抛出的是受检异常而事务只回滚RuntimeException。面试官问事务失效的场景你只需要回答出三到四个典型的例子就会显得你实战经验丰富。6.3 SpringBoot自动配置原理从spring.factories到条件装配SpringBoot的自动配置是SpringBoot最核心的卖点也是面试中的高频考点。考察方式是让你解释为什么引入一个starter之后对应的功能就自动生效了不需要写一堆配置类。自动配置的核心在EnableAutoConfiguration注解。SpringBoot启动时会通过Import把AutoConfigurationImportSelector这个类引入它会读取classpath下的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件Spring Boot 2.7之前是META-INF/spring.factories文件加载文件中列出的所有自动配置类。关键环节在于这些自动配置类并不都会生效它们被Conditional系列条件注解标注比如ConditionalOnClass只有在classpath存在指定类时才生效ConditionalOnMissingBean则是在当前容器中没有指定Bean时才生效。这就是条件装配SpringBoot通过这种方式实现了按需加载。你在回答这个问题时如果能主动提一句“EnableAutoConfiguration配合Conditional注解既保证了框架的自动装配能力又保证了用户自定义Bean的优先权”会让面试官觉得你不只是看过源码而是理解了这种设计模式的精妙之处。7. 面试实战答题逻辑、追问应对与心态管理把知识点都复习完了最后一步是学会在面试现场把知识有效地“输出”出来。很多候选人不是不会而是答得太散东一句西一句本来一个很好的回答因为逻辑混乱被面试官打低分。这部分我重点讲讲答题的组织方式。7.1 源码题的回答框架结论优先再展开细节我观察到一个普遍规律同样一个HashMap的问题能拿高分的候选人通常遵循“结论、展开、总结”的答题路径。比如面试官问“HashMap线程安全吗”很多人的第一反应是直接答“不安全可以用ConcurrentHashMap”然后就没有然后了。但高手会这样回答先给结论——HashMap在多线程环境下不安全因为扩容时可能出现链表环JDK 1.8修复了环的问题但仍然存在数据丢失和覆盖问题。再展开讲一个典型场景——两个线程同时put如果计算出的数组下标相同其中一个线程的值可能被覆盖如果两个线程同时触发扩容重排哈希时也可能丢失数据。最后给出解决方案——使用Collections.synchronizedMap包装或者使用ConcurrentHashMap并解释为什么ConcurrentHashMap性能更好。这套框架的好处是面试官能第一时间知道你的答案方向又有足够的深度证明你真正理解最后还能展示你的技术选型能力。7.2 主动引导面试官到你熟悉的领域面试中有个比较少有人用但非常有效的技巧每回答一个问题之后有意识地抛出一个你准备充分的关联知识点。比如面试官问你ArrayList和LinkedList的区别你回答完选型之后可以主动说一句“其实在实际项目中并发场景下还有一个CopyOnWriteArrayList可以用来替代ArrayList它能解决遍历时修改集合抛异常的问题”。面试官很可能顺势追问CopyOnWriteArrayList的原理而这个问题你恰好准备得非常充分就顺利把面试引导到了你的优势区域。这个技巧的核心是“给面试官递话题”不是转移话题而是在回答完对方的问题后提供额外的信息增量。前提是你对自己的准备情况有清晰的认知只引导到你有把握的区域不要为了炫技抛出一个自己半懂不懂的概念。7.3 面试后的复盘方法每次面试结束建议趁记忆还清晰的时候把没答上来的题记录下来分类整理。我会把这些题分成三类完全没思路的、知道一部分但讲不透的、回答得不错但可以更好的。完全没思路的题说明存在知识盲区需要补充学习讲不透的题说明某个知识点的深度不够需要重新读源码回答得不错的题则可以作为后续面试的保留回答框架。复盘时还有一个容易被忽略的维度记录面试官追问的方向。面试官追问哪里说明他认为哪里重要也说明他关注的能力点在哪里。多复盘几次你会逐渐摸清不同岗位对候选人的能力偏向后面的面试会越来越有把握。我在实际准备和辅导过程中最深的体会是准备Java面试不要把自己当成在背题的考生而要当成一个在梳理知识体系的技术人。你每搞懂一个底层原理不只是多了一道面试题的答案而是你的整个知识网络又多了一个可以互相连接的节点。把基础、集合、并发、JVM、框架这几个模块串联成一张网在面试中你会发现很多看似独立的题目底层逻辑其实是相通的。
返回列表