
1. 面试前夜Java实习岗位到底在考什么模拟面试做了几十场从最初的紧张磕巴到现在能跟候选人把一个问题聊到源码层面我最大的感受是Java实习面试真不是单纯背八股文。面试官问集合、并发、JVM、Spring表面上看是在考察基础知识实际上是在考察你“有没有计算机功底、有没有源码嗅觉、有没有排查问题的思路”。很多候选人背了一堆概念但一被追问就露馅原因很简单他背的是“结论”而不是“推导过程”。这篇内容我把近几年Java实习岗模拟面试中最高频、最容易连环追问的题目做一个深度拆解覆盖集合框架、并发编程、JVM内存与调优、Spring核心原理四个大方向。每一道题我都会站在面试官视角告诉你他想听到什么、你该往哪个方向答、哪些话说了反而扣分。如果你正在准备Java实习、校招提前批或者刚工作一年想查漏补缺这篇内容可以直接当模拟面试脚本用。需要提前说明的是这篇内容里的追问方式来自我个人的面试和模拟面试实践不同公司、不同面试官风格差异很大但考察的底层逻辑是共通的——知识点要成体系不能是孤岛。2. 集合框架高频题背后的数据结构功底2.1 HashMap不只是“键值对”这么简单面试官问“你了解HashMap吗”这题听起来特别基础恰恰是最容易拉开差距的题。实习候选人通常答“HashMap是基于哈希表的Map接口实现允许null键和null值非线程安全”这个答案只能算及格完全不够。我面试时通常会追问三层第一层HashMap在JDK 1.7和1.8之间的数据结构变化。这个必须答出来——1.7是数组加链表1.8是数组加链表加红黑树链表长度超过8且数组长度大于等于64时转红黑树否则优先扩容。为什么要转红黑树因为链表查询是O(n)红黑树是O(log n)极端哈希冲突时性能退化严重。第二层哈希函数是怎么设计的。高16位异或低16位(h key.hashCode()) ^ (h 16)这步的作用是什么——让高位信息也能参与到数组下标的计算中降低哈希冲突概率。然后(n - 1) hash等价于取模运算前提是数组长度必须是2的幂次。这个“为什么容量是2的幂”几乎必问。第三层put方法底层流程。我要求候选人能完整口述出来先对key做哈希扰动然后判断table是否为空为空则resize再根据(n - 1) hash定位到数组位置如果位置上没有元素直接放入如果有元素则比较哈希值和equals方法如果冲突则尾插法1.8以后追加到链表尾部或树化最后判断size threshold决定是否扩容。这里有一个细节很多人漏掉——JDK 1.7的扩容是头插法多线程环境扩容时会产生环形链表导致get死循环这也是HashMap非线程安全最经典的例证。我再给一个容易被连环追问的细节扩容时元素是怎么迁移的。1.7是重新计算每个元素的hash和位置1.8做了优化通过e.hash oldCap判断元素留在原位置还是移动到“原位置oldCap”的新位置。这个优化很巧妙既省去了重新计算哈希的开销又保证了迁移后链表顺序不反转。能答到这里面试官基本会认为你真正读过源码。提示HashMap允许null键null键的hash固定为0所以null键会放在数组下标为0的位置。这个细节虽然小但能体现你对源码的熟悉程度。2.2 ArrayList扩容机制最容易被连环追问的“简单题”ArrayList的扩容机制属于那种“面试官觉得你应该会但很多人答不完整”的题。标准答法是内部是Object数组默认初始容量10添加元素时如果size 1 elementData.length就扩容新容量是旧容量的1.5倍oldCapacity (oldCapacity 1)然后Arrays.copyOf拷贝到新数组。但面试官不想只听到这个他会继续追问为什么是1.5倍而不是2倍或者1.2倍这个要结合扩容的均摊复杂度来分析。如果每次扩容增加固定大小N那频繁扩容时均摊成本太高不好如果倍数太大比如2倍内存浪费严重——扩容后可能有大量闲置容量只能等缩容而ArrayList默认不缩容。1.5倍是一个空间和时间上的折中且oldCapacity 1是位运算效率高。严格说JDK没有官方解释为什么是1.5倍但你要能从均摊分析的角度说出一二。线程安全问题怎么处理?ArrayList非线程安全多线程并发add会数据错乱甚至数组越界用Collections.synchronizedList包装或者在并发场景下用CopyOnWriteArrayList。这里通常面试官会顺势从集合跳到并发——ArrayList的fail-fast机制值得展开迭代器遍历时如果结构被修改modCount变了会立即抛出ConcurrentModificationException但注意这依赖的是“预期modCount和实际modCount的对比”是一个“尽力而为”的快速失败机制不是可靠保证。ArrayList和LinkedList的差异也是常客。虽然实际项目里LinkedList用得少但概念必须清晰ArrayList基于数组随机访问O(1)尾部插入删除O(1)中间插入删除O(n)LinkedList基于双向链表随机访问O(n)头部和中部插入删除是O(n)因为要先找到位置——很多人以为LinkedList插入一定比ArrayList快这是误区。真正对比要看你操作的位置和遍历开销实际应用中数组的局部性更好所以ArrayList绝大多数时候性能更优。2.3 ConcurrentHashMap集合与并发的双重考点ConcurrentHashMap是面试中的“集大成者”因为它既考集合又考并发。我一般从线程安全这个切入点开始问要求候选人对比HashMap、Hashtable、ConcurrentHashMap三者的线程安全实现。答到JDK 1.8的ConcurrentHashMap时核心是CAS加synchronized初始化数组时用CAS保证只有一个线程能完成初始化put时如果对应桶位是空的用CAS直接写入如果桶位非空则对头节点加synchronized锁住这个桶。锁粒度从1.7的Segment段锁默认16个Segment相当于把整个Map分成16段各自加锁细化为单个桶级锁并发度大幅提升。有一个细节很多人不理解为什么1.8还用synchronized而不是直接用ReentrantLock答案是JDK对synchronized做了大规模优化——偏向锁、轻量级锁、锁升级机制、锁粗化、自适应自旋在低竞争场景下synchronized的性能已经不输甚至优于ReentrantLock而且synchronized不需要手动释放锁代码更简洁、不容易出错。面试官听到这里会眼睛一亮说明你对“锁的性能演进”有认知。另外ConcurrentHashMap有几个常考的点size()方法怎么统计全局元素数量1.8里面是维护一个baseCount加上CounterCell数组来分散计数压力类似LongAdder的思想避免高并发下只有一个计数器被争抢扩容时怎么支持多线程协助迁移元素它会通过transferIndex把任务拆分成多个子区间让多个线程各自负责一段数组区间的迁移也就是“多线程并发扩容”。我建议候选人在回答ConcurrentHashMap时主动对比一下Hashtable为什么性能差——每个方法都加synchronized锁锁的是整个表对象读也要竞争锁并发效率极低。这种对比式答法能体现出你的知识是串成网的不是背题。3. 并发编程从八股到“能干活”3.1 synchronized的锁升级从偏向锁到重量级锁并发方向我最常问的第一个题就是“synchronized的实现原理”。很多候选人上来就背synchronized是JVM内置锁基于Monitor对象实现可以通过三种方式使用……但面试官真正想听到的是锁升级的那套机制。JDK 1.6以后synchronized不再是“一上来就是重量级锁”而是根据竞争情况逐步升级无锁 → 偏向锁 → 轻量级锁 → 重量级锁。偏向锁会记录持有锁的线程ID存在对象头的Mark Word里如果一直是同一个线程获取锁就省去CAS操作的开销一旦有第二个线程竞争偏向锁撤销升级为轻量级锁轻量级锁用自旋CAS循环重试来等待锁释放适合锁持有时间很短的场景如果自旋超过一定次数或者竞争线程数超出就膨胀为重量级锁线程会阻塞并进入Monitor的等待队列涉及用户态和内核态的切换开销最大。这个机制怎么答才加分我建议你顺带把“自适应自旋”讲一下——JVM会根据上一次在同一个锁上自旋等待的时间动态调整自旋次数。如果某个锁几乎不产生竞争自旋也许很快就放弃如果上次自旋成功这次可能多自旋一会儿。这个细节能明显体现源码级认知。synchronized的锁对象是存在哪里——对象头。类级别锁static方法锁的是Class对象普通方法锁的是this实例。这些基础信息也要准确。还有可重入性同一个线程可以重复获取同一把synchronized锁JVM在线程获取锁时会记录锁的持有线程和计数器重入时计数加一释放时递减。3.2 volatile和JMM内存可见性的底层逻辑volatile是Java并发里最让人迷惑的关键字。我的经验是能三句话讲清楚volatile的人并发基础基本扎实。哪三句话第一volatile保证被修饰变量的可见性——一个线程修改了变量值其他线程能马上看到第二volatile禁止指令重排序第三volatile不保证原子性。说完这三句话之后关键是往深了解释。可见性为什么靠volatile就能保证这涉及Java内存模型JMM。JMM规定线程对变量的操作基于主内存和各线程私有工作内存实际对应CPU缓存、寄存器等线程操作变量前需要从主内存load到工作内存操作完成后再store回主内存。如果不用volatile这个write-back时机不可控其他线程可能读到的永远是旧值。volatile变量会让JVM在读写时插入内存屏障Memory Barrier保证写volatile变量时编译器不会把之前的普通写操作排到后面去读volatile变量时之后的普通读不会被排到前面来。这就是“禁止重排序”的实现机制。再往深一层就是著名的DCLDouble-Checked Locking单例模式问题。经典代码public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }为什么要加volatile因为new Singleton()不是原子操作——它有三步分配内存、初始化对象、将引用指向内存地址。CPU和编译器可能重排序为“分配内存→引用指向内存地址→初始化对象”这时如果另一个线程进来了第一次检查instance ! null直接返回了一个还没初始化完成的对象。加了volatile之后重排序被禁止从根本上避免了“拿到半初始化对象”的问题。这个例子是面试官最喜欢用来考volatile实际应用的值得每个候选人熟练掌握。3.3 线程池核心参数怎么配、拒绝策略怎么选线程池是并发方向“性价比最高”的考点因为它是项目里真正会用到的内容。问题通常是两大类一是线程池的核心参数有哪些、含义是什么、怎么配置二是execute流程怎么走、拒绝策略有哪些。七个核心参数corePoolSize核心线程数、maximumPoolSize最大线程数、keepAliveTime非核心线程空闲存活时间、unit存活时间单位、workQueue任务队列、threadFactory线程工厂、handler拒绝策略。执行流程核心线程数未满时直接创建核心线程执行任务满了之后新任务进队列队列也满了才创建非核心线程达到最大线程数且队列满了触发拒绝策略。这里有三个细节候选人经常答错。第一队列和最大线程数的关系——很多人以为是“先创建非核心线程队列满了再拒绝”实际上顺序是核心线程→队列→非核心线程→拒绝。第二四种拒绝策略的内容AbortPolicy默认抛RejectedExecutionException、CallerRunsPolicy调用者线程自己执行起到背压作用、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃队列中最旧的任务。第三ThreadPoolExecutor的构造函数里核心线程数默认是可以被回收的除非设置allowCoreThreadTimeOut(true)。线程池大小怎么配这个没有标准答案但有合理思路——CPU密集型任务建议CPU核数 1IO密集型任务建议CPU核数 * 2或者CPU核数 / (1 - 阻塞系数)。为什么IO密集型可以配大因为IO等待期间CPU是空闲的可以让更多线程执行计算。实际项目里我建议用压测而不是公式硬套公式只是起点。面试官如果问“你们项目里线程池是怎么封装的”建议如实回答是不是用了Executors工具类的固定方法。这里有一个大坑——Executors.newFixedThreadPool用的是无界LinkedBlockingQueue队列可以无限增长任务积压太多会导致内存溢出newCachedThreadPool用的是SynchronousQueue最大线程数是Integer.MAX_VALUE线程创建过多会耗尽系统资源。所以在生产环境中更推荐直接用ThreadPoolExecutor手动指定参数含义清晰、可控性强。3.4 ThreadLocal内存泄漏和父子线程传递ThreadLocal在实习面试里没有前面几个那么高频但只要问到基本就是“致命追问”。你至少要答出ThreadLocal为每个线程维护一份独立的变量副本底层是Thread类内部有一个ThreadLocalMapkey是ThreadLocal对象弱引用value是线程持有的变量副本。致命追问来了ThreadLocal为什么会内存泄漏因为key是弱引用value是强引用。当外部不再持有ThreadLocal对象时ThreadLocalMap里的key会被GC回收变成null key但value仍然被ThreadLocalMap强引用着——如果这个线程长期存活比如线程池里的线程value就永远不会被回收造成“逻辑泄漏”。解决方案就是用完之后调用remove()方法。还有一个扩展考点ThreadLocal怎么实现线程间值的传递面试官如果问这个其实是在问你有没有用过InheritableThreadLocal。InheritableThreadLocal可以在创建子线程时把父线程的ThreadLocal值拷贝给子线程。但注意它只对“创建时”生效线程池里线程复用后新提交的任务不一定能拿到最新的父线程值——这个场景需要Ali的TransmittableThreadLocal来解决。能聊到这一层的实习生基本已经超越80%的候选人了。4. JVM内存模型、GC和调优的面试通用打法4.1 JVM内存区域哪些是线程私有哪些是线程共享JVM是Java面试中“背书感”最重的板块但只要掌握一套清晰的划分逻辑就不会乱。直接背区域然后背每块区域存什么。线程私有的三个区域程序计数器Program Counter Register、虚拟机栈VM Stack、本地方法栈Native Method Stack。线程共享的两个区域堆Heap、方法区Method AreaJDK 1.8之后实现为元空间Metaspace。虚拟机栈是重点每个方法调用对应一个栈帧栈帧里面有局部变量表、操作数栈、动态链接、方法返回地址。局部变量表存的是基本数据类型和对象的引用注意不是对象本身所以如果问对象到底在堆里还是栈里要注意区分——“对象的引用在栈里局部变量对象实例在堆里”。栈随着方法调用的结束自动回收不需要GC。堆内存的分代结构也要说清楚新生代Eden区、两个Survivor区默认比例8:1:1但JVM会根据实际使用动态调整和老年代。绝大多数对象先在Eden区分配Minor GC之后存活的对象进入Survivor区每次Minor GC熬过一岁年龄加1年龄达到默认15次后晋升老年代。为什么是15因为对象头里记录分代年龄的bit位是4位最大15可以通过-XX:MaxTenuringThreshold调整。4.2 对象创建到GC回收一条完整链路面试官如果问“一个Java对象从创建到被回收的完整过程”你必须从字节码指令new开始讲起。类的加载检查、分配内存指针碰撞或空闲列表取决于堆是否规整、内存空间初始化零值这步很关键保证实例字段在不赋值时也有默认值、设置对象头Mark Word存哈希码、GC分代年龄、锁状态标志等、执行构造方法。对象怎么判定“已死”两种算法引用计数法和可达性分析算法。引用计数法有循环引用问题两个人互相引用但外部无人使用计数器永远不为0所以HotSpot用的是可达性分析——从GC Roots出发能遍历到的对象就是存活对象遍历不到的都算垃圾。GC Roots有哪些栈帧中本地变量表引用的对象、方法区中类静态属性引用的对象、常量引用的对象、JNI引用的对象、JVM内部的Class对象等。垃圾收集算法有四种按历史演进说标记-清除、复制、标记-整理、分代收集。标记-清除有碎片化问题、效率也不高复制算法浪费一半空间但8:1:1的Survivor设计就是为了把浪费控制在10%以内标记-整理适合老年代把存活对象往一端移动消除碎片分代收集则是把不同代用不同算法的组合思路。现代垃圾收集器像CMS、G1、ZGC要能说出各自的特点和适用场景——G1的Region化布局、可预测停顿、RSetRemembered Set记录跨Region引用ZGC的颜色指针和读屏障这些是加分项。关于GC触发时机Minor GC是新生代满了触发Full GC是老年代满了或元空间不足或System.gc()显式调用等。如果回答Full GC的触发条件时能补充一句“在排查线上问题时Full GC频繁往往和对象过早晋升、大对象直接进入老年代、元空间溢出这几个因素有关”就能体现实战意识。4.3 类加载与双亲委派不止是“背下来”类加载机制常考三个点加载过程分几步、双亲委派模型是什么、为什么需要双亲委派。类加载流程加载通过类名获取二进制字节流、解析为方法区的运行时数据结构、在堆中生成Class对象、验证、准备为静态变量分配内存并初始化为零值注意final static的常量直接赋值、解析符号引用替换为直接引用、初始化执行clinit方法给静态变量赋真实初始值。双亲委派模型当一个类加载器收到类加载请求时先不自己加载而是委派给父加载器逐级向上到Bootstrap ClassLoader父加载器加载不了才往下回退。好处有两点确保核心类库的类不会被自定义类覆盖比如你写一个java.lang.String无法替代JDK自带的String防止类被重复加载。面试官问到“哪里会破坏双亲委派模型”这是个深度试水题。典型场景包括JDBC的SPI机制Service Provider Interface——DriverManager由Bootstrap ClassLoader加载但Driver接口实现由各个数据库厂商提供线程上下文类加载器Thread Context ClassLoader绕过双亲委派来加载这些实现类Tomcat的WebAppClassLoader每个Web应用有独立的类加载器以实现应用隔离优先加载自己WEB-INF/classes下的类。这些内容实习岗可能不会问到但心里有底和完全没有是两回事。4.4 常见OOM场景和JVM调优思路面试官问“你遇到过OOM吗”很多人说“没有”这个回答其实不太聪明。你可以说“生产环境我接触不多但我理解常见的OOM场景有以下几类并且我知道排查思路……”然后把知识点展开。常见的OOM有Java Heap Space堆内存不足对象创建太多、存在泄漏、GC Overhead Limit ExceededGC回收率很低但回收效果差JVM处于“回收也白回收”的循环、Metaspace元空间不足被CGLIB动态生成的类太多、Unable to create new native thread操作系统的线程数被耗尽。排查思路基本是固定套路先用jps找到进程号再用jstat看GC情况用jmap导出堆转储快照然后用MAT或VisualVM分析大对象和引用链。如果是CPU飙高用top查看线程、再用jstack导线程快照定位到问题线程。这个套路一定要熟练最好能说出“线上环境jmap有风险要考虑对业务的影响一般先在测试环境复现或者用jmap加-dump:live参数只导出存活对象”这类实战细节。JVM调优不是“面试背几个JVM参数就完事”的事情。需要掌握的常用参数-Xms初始堆大小、-Xmx最大堆大小、-Xmn新生代大小、-XX:MetaspaceSize和-XX:MaxMetaspaceSize、-XX:UseG1GC切换G1、-XX:MaxGCPauseMillis设置停顿时间目标、-XX:SurvivorRatio设置Eden和Survivor比例、-XX:HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath这几个是必备。调优的顺序不能乱先明确目标是降低停顿还是提高吞吐量再监控和采集数据最后才是调整参数和验证效果。5. Spring核心IOC容器、循环依赖与事务失效5.1 Spring Bean的生命周期从扫描到销毁Spring方向的面试题基本绕不开Bean生命周期。这道题如果答得好能直接带出IOC、AOP、循环依赖等一系列知识点属于“一题顶十题”的题眼。Spring Bean的生命周期完整的节点大致是实例化Instantiation→ 属性填充Populate Properties→ 感知回调BeanNameAware、BeanFactoryAware、ApplicationContextAware→ BeanPostProcessor的前置处理postProcessBeforeInitialization→ InitializingBean接口或PostConstruct注解或自定义init-method → BeanPostProcessor的后置处理postProcessAfterInitializationAOP的动态代理就在这一步→ Bean就绪可以使用 → 销毁阶段DisposableBean、PreDestroy、自定义destroy-method。面试官通常会在AOP代理处追问“AOP是在Bean生命周期的哪个阶段做的”答案就是postProcessAfterInitialization。Spring在容器中如果有匹配当前Bean的Advice和Pointcut就会生成代理对象。如果你对答案有自信可以主动提一句“实际是AbstractAutoProxyCreator这个BeanPostProcessor实现类在起作用它会在postProcessAfterInitialization里调用wrapIfNecessary”这代表你真的看过Spring源码而不是背总结文章。5.2 三级缓存与循环依赖近两年最爱的连环追问Spring循环依赖几乎成了Java面试的保留节目。场景是A依赖BB依赖A如果不处理实例化A时发现要注入B实例化B时发现要注入A互相等对方死循环。Spring的解法是三级缓存这是一个容易引起争议的话题但面试里标准的回答仍然是三级缓存一级缓存singletonObjects存放完整的单例Bean。 二级缓存earlySingletonObjects存放提前暴露的早期Bean半成品属性还没填充完但对象引用已经可以被别人拿到。 三级缓存singletonFactories存放Bean的ObjectFactory是一个函数式接口调用getObject可以生成早期引用。创建A时实例化A → 放入三级缓存的ObjectFactory → 填充属性发现需要B → 去创建B → B填充属性时发现需要A → 从三级缓存拿到A的早期对象ObjectFactory.getObject()可能返回经过AOP代理的引用放入二级缓存 → B拿到A的引用继续完成创建 → B创建完成后A拿到B的完整Bean继续填充属性 → A创建完成后把自己移入一级缓存。关键问题一为什么需要三级缓存二级不行吗这是考察点。如果没有AOP二级缓存就够了——提前把A的实例暴露出去就行。但因为AOP的存在Bean可能在创建过程中被代理如果只用二级缓存在实例化后立刻生成代理对象放入二级缓存那可能会让没有参与循环依赖的Bean也提前被代理与Spring的“最后代理”的设计相悖。三级缓存放的是ObjectFactory延迟到真正需要早期引用时才调用getObject来生成可能生成代理既解决了循环依赖又避免了所有Bean提前代理是一种“按需代理”的延迟策略。能说到这里面试官基本满意。关键问题二循环依赖能解决所有场景吗不能。以下情况会打破循环依赖的支持构造器注入的循环依赖无法解决因为构造器注入在实例化阶段就需要依赖对象此时对象还没实例化三级缓存里没有东西可拿非单例的Beanprototype作用域不缓存循环依赖无解Async注解的Bean因为本身就可能需要代理情况更复杂可能报CurrentProxyReferenceException。5.3 Spring事务失效的坑必问的实战细节Spring事务管理是实习面试的高频区但比“事务传播行为有哪几种”更常考的是“什么情况下事务会失效”。我直接列高频失效场景每个都值得展开。第一类内部调用。同类中methodA调用了methodB且methodB标了Transactional事务不生效。因为Spring事务是基于AOP代理实现的内部调用直接走的是this对象的方法没有经过代理对象事务注解自然无效。解决办法是把调用改成从Spring容器中获取代理对象如注入self或者拆到另一个Bean里。第二异常被捕获未抛出。方法里try-catch吞掉了异常事务感知不到不会回滚。第三抛出的不是RuntimeException。Spring默认只对RuntimeException和Error回滚如果抛出受检异常Exception的子类且没有在Transactional的rollbackFor属性里指定事务不会回滚。这里有一个记忆技巧默认回滚对“非受检异常”生效目的就是让可恢复的业务异常不一定需要回滚但很多团队习惯把所有异常都设置为回滚即rollbackFor Exception.class。第四加了Transactional的方法不是public的。Spring的声明式事务默认基于代理private、protected方法不会走代理事务不生效。CGLIB虽然可以代理非public方法但Spring官方文档明确推荐只标注在public方法上。第五数据库表引擎不支持事务。比如MySQL的MyISAM引擎不支持事务你注解标得再好也没用必须InnoDB。这个点看着很傻但实际面试官特别喜欢拿它来测试候选人有没有“数据库层面”的认识。5.4 Spring三级缓存误区的澄清我很想单独说一段关于三级缓存的误区。现在网上很多文章把三级缓存和循环依赖混为一谈导致候选人回答时出现两个极端要么觉得三级缓存的存在就是为了解决循环依赖要么觉得没有循环依赖三级缓存就毫无意义。准确地说Spring三级缓存中的每一级都有存在的现实意义。一级缓存就是标准的单例池保证单例Bean只实例化一次这个不用多说。二级缓存是“早期引用缓存”当Bean还在创建过程中需要暴露引用给其他Bean时就从这里取。三级缓存则是利用ObjectFactory实现延迟生成如果Bean的最终形态不需要代理即没有AOP干预三级缓存的ObjectFactory直接返回原始实例也不会产生任何额外开销。所以“三级缓存是为了解决循环依赖”对但不精确——它真正解决的是“在循环依赖场景下早期暴露的Bean还能是AOP代理后的Bean”这一组合问题。面试时把这一层说透很容易成为整场面试的高光点。6. 企业模拟面试复盘一次完整的高频追问模拟为了让你有更直观的体感我写一段模拟面试实录。面试官是“我”候选人是一位目标实习岗的应届生。这个对话不是逐字稿但情景来自很多次真实模拟。面试官你说一下HashMap的put流程。候选人先计算key的hash然后定位数组下标如果数组为null先扩容如果这个位置没有元素直接放入有元素就判断hash和key是否相等不相等就追加到链表尾部或转红黑树最后判断是否超过阈值决定要不要扩容。面试官为什么有红黑树为什么阈值是8?候选人链表太长查询效率低红黑树查询是O(log n)。阈值是8是因为泊松分布算出来在加载因子0.75、哈希函数正常的情况下链表长度到8的概率已经非常低这时候转树综合收益最高。面试官HashMap为什么是线程不安全的能不能举一个具体场景候选人两个线程同时put都命中了同一个数组下标为null的位置一个线程写完另一个线程又覆盖了数据就丢了。更严重的是1.7扩容时的头插法多线程下可能形成环形链表之后get访问到那个桶就直接死循环。面试官如果让你用一个线程安全的Map你会怎么选候选人并发度极高的场景用ConcurrentHashMap它是分段锁或CAS加synchronized性能很好如果只是简单包装可以Collections.synchronizedMap如果读多写少可以考虑CopyOnWriteMap但数据一致性要求高的话不太合适。面试官那ConcurrentHashMap的size()是怎么实现的候选人JDK 1.8里先尝试用CAS更新baseCount如果竞争激烈就通过CounterCell数组把计数打散每个线程更新自己的CounterCell最后sum这些Cell和baseCount。这样避免了所有线程都去抢一个计数变量。面试官果然不错那你说说Spring是怎么解决循环依赖的候选人靠三级缓存。一级存完整Bean二级存早期暴露的引用三级存ObjectFactory。A创建时提前把ObjectFactory放进三级缓存B创建时依赖A从三级缓存调getObject拿到A的早期引用放入二级缓存B完成后再回头给A注入……面试官那三级缓存为什么必须存在二级缓存行不行候选人如果不需要AOP代理二级缓存是够用的。但Spring要保证循环依赖中拿到的早期对象也是符合AOP要求的代理对象。三级缓存用ObjectFactory延迟生成只有当真正发生循环依赖、需要提前暴露时才会调用getObject生成代理其他Bean在正常创建流程中不会提前代理。这样就可以兼顾循环依赖和AOP的创建顺序。面试官最后一个问题一个volatile的int变量两个线程同时执行i结果会怎么样候选人i不是原子操作它相当于读i、加1、写回i三步。两个线程交错执行最终结果可能比预期小。比如i从10开始A读到10B也读到10A写回11B也写回11最终i11而不是12。volatile只能保证可见性和禁止重排不能保证原子性。要原子递增需要AtomicInteger底层CAS或者加synchronized。这个模拟对话里你能看到面试官的追问节奏非常密从map到并发从并发到Spring每个问题的下一步都来自你的上一个回答。这也是我反复提醒候选人“不要背孤立知识点”的原因。7. 准备阶段的独家建议与常见问题速查最后说说我在准备和模拟中发现的一些共性问题和对应策略按踩坑频率排序。第一个问题是“背了框架源码但说不清使用场景”。比如问ConcurrentHashMap的适用场景答案除了“并发环境下的Map”之外至少要补充“和普通同步Map的对比、和高频写读操作场景的匹配程度”。面试官不需要一个只会背书的人他需要你把知识映射到场景里。第二个问题是“回答没有主次颠三倒四”。语言组织一定要用“总-分-总”或者“结论-原因-举例”的结构。推荐一个答题模板先说结论一句话概括再说核心原因1到3个要点最后补充一个细节或案例。比如“HashMap线程不安全主要有两点原因一是多线程put时数据覆盖丢失二是1.7扩容时头插法可能在并发下产生环形链表导致死循环。”第三个问题是“主动挖坑”。有的候选人事先准备了很多自以为高深的知识面试时强行往话题上靠。这个非常忌讳因为你一旦抛出一个面试官不熟悉或者你只是知其然不知其所以然的概念对方一定会追问到你崩溃。如果你要抛高级话题前提是你真的能把原理讲到数据库源码级别否则宁可少说。第四个共性问题是对“Intern方法、泛型擦除、注解、反射”这类基础过程了解不足。集合、并发、JVM、Spring属于“框架层”知识如果反射、泛型这类“语法层”知识不扎实面试官会觉得你基础没有形成闭环。建议准备阶段把反射Class对象、获取构造器/方法/字段、动态代理和泛型类型擦除、桥方法单独过一遍。我把高频问题做成速查表方便你面试前一小时快速翻阅。面试内容高频题目回答关键集合HashMap数据结构、put流程、扩容机制1.7链表1.8红黑树、扰动函数、尾插法、1.5倍扩容集合ConcurrentHashMap线程安全原理CASsynchronized、桶级锁、CounterCell计数并发synchronized锁升级过程无锁→偏向锁→轻量级锁→重量级锁、自旋、Monitor并发线程池核心参数和执行流程核心线程→队列→非核心线程→拒绝策略JVM内存区域划分程序计数器、栈、本地方法栈、堆、元空间JVM如何判断对象可回收可达性分析、GC RootsJVMOOM排查思路jps→jstat→jmap→MAT、jstack查线程SpringBean生命周期实例化→属性填充→Aware回调→BeanPostProcessor→init→使用→destroySpring循环依赖三级缓存三级缓存各自存什么、为什么三级不能合并Spring事务失效场景内部调用、吞异常、受检异常、private方法、引擎不支持事务准备策略上我再给一个“三个晚上”的方法论。第一个晚上专门读集合源码HashMap的put/resize/get全流程、ConcurrentHashMap的核心方法、ArrayList的add/remove/grow、LinkedList的结构。第二个晚上过并发synchronized和锁升级、volatile和JMM、线程池和拒绝策略、完全理解ThreadLocal。第三个晚上攻JVM和Spring内存区域、GC算法和收集器、类加载双亲委派、Bean生命周期、循环依赖和事务失效。每个知识点都要用上面的“结论-原因-例子”结构自己对着镜子讲一遍讲不通的就是你要回去补的。8. 写在最后面试之外的思考我个人带过不少实习生的体会是模拟面试真正提升的不是“背题能力”而是“在压力下组织已知知识的能力”。很多人不是不会是一紧张就一股脑倒豆子想到什么说什么没有结构。所以我自己面试前一定会做的一件事是把JVM、集合、并发、Spring四个方向的关键词写在纸上每半天自己对着纸“讲课”5分钟——你不需要说得全对但你需要说得连贯通顺。从拿offer的角度来看实习面试的通过标准不在于你是不是把源码全部背下来而在于面试官能不能确认基础的你会深入的你敢想复杂问题的思路是有逻辑的。这本就不是一晚上能突击出来的但确实可以通过方法和刻意练习在一个月内形成质变。如果你正在准备近期面试我建议你从这篇内容里挑两个自己最不熟的题目先用自己的话口述一遍再对照文章里的思路逐条补充。我能写出来的都是被反复验证过的回答路径但你真正能够输出多少取决于你练了多少遍。祝顺利。