ARTICLE DETAIL

资讯详情

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

Java后端面试官视角:并发编程、Spring与MyBatis高频考点拆解

Java后端面试官视角:并发编程、Spring与MyBatis高频考点拆解 2026年的校招季还没正式开始我已经在帮两三个团队做技术面试官的预演了。筛了几百份简历之后有个特别直接的感受Java后端这个赛道校招和三年以内的社招大厂问来问去就盯三个方面——并发编程、Spring、MyBatis。题量看着很大什么“500道高频题”满天飞但真正拉开差距的从来不是题数是你能不能从一句话答案讲到原理链路和边界条件。这篇文章从一个面试官和过来人的双重角度把这三块里最值得精读的高频考点拆开讲。目的不是让你背题是让你知道每个考点背后面试官在验什么能力以及怎么组织你的答案。文末我也会讲这套500道题应该怎么分层刷、怎么模拟面试才不会白费功夫。适合正在准备大厂面试的Java后端候选人也适合带新人的技术组长参考提问角度。1. 面试官视角这三个技术栈到底在考什么能力很多人刷题刷得昏天黑地却从来没想过面试官为什么总挑这三块问。我做了几十场模拟面试总结下来这三个技术栈恰恰对应了后端工程师的三层核心能力。1.1 并发编程考的是多线程下的“正确性直觉”并发题很少有单纯背概念的。面试官给你一个场景——一个计数器、一个缓存、一个订单状态流转——然后问你“这个写法有没有问题”。他要验证的是你有没有形成一种本能看到共享可变状态第一反应就是问三个问题——可见性保证了吗原子性保证了吗会不会死锁或者活锁Java内存模型里那条 happens-before 规则能不能脱口而出直接决定了你能不能接住后续的追问。我见过不少候选人synchronized 和 volatile 的定义背得滚瓜烂熟但给他一段双线程累加的代码就说不出哪里有问题这种就是典型的没有形成正确性直觉。1.2 Spring考的是框架运行时的“翻译能力”Spring 题目最迷惑人的地方在于IoC、AOP 这些概念人人都会说但面试官真正想听的是你把概念“翻译”成运行时行为的能力。比如他问“Spring 怎么管理 Bean 的生命周期”不是要你回答“实例化-初始化-销毁”六个字而是要看你能不能讲出 BeanDefinition 在哪里加载、构造器是在哪个阶段调用的、属性填充发生在哪一步、 Aware 接口在什么时候被回调、AOP 代理对象又是通过哪个后置处理器在哪个节点生成的。能把这套链路讲顺的人写代码时心里是有一张运行时地图的遇到循环依赖、事务失效、代理没生效这类问题排查速度完全不一样。1.3 MyBatis考的是 ORM 层之上的“排查与扩展能力”MyBatis 在大厂面试里地位很特殊简单但坑极多。面试官用它来验证你平时是不是真的在写生产代码——比如线上出现慢查询你能不能从 MyBatis 的配置层面快速定位问题一个 Mapper 里的查询查不到缓存你能不能说清楚一级缓存和二级缓存的生命周期你要做数据权限、多租户、分页这些通用能力有没有想过用 MyBatis 插件去做。说白了Spring 考你的框架理解MyBatis 考的是你有没有在框架之上做增强和排查的实战感。1.4 那500道题是怎么分层的先交代一下这套题的构成方便你后面按图索骥。我整理的高频题库大概长这样模块大致题量细分维度并发编程约180道Java内存模型、synchronized/锁、AQS与JUC容器、线程池、ThreadLocal、CompletableFutureSpring约200道Bean生命周期、三级缓存、AOP、事务、Spring Boot自动配置、Spring Cloud核心组件MyBatis约120道基础SQL与占位符、一级/二级缓存、插件机制、Spring整合、手写核心链路这套题我从来不让候选人一口气刷完那样刷完也是散的。下面的章节我会把每个模块里“背题最容易糊弄过去”的高频考点单独拉出来讲讲背后的原理和答题边界。2. 并发编程高频题拆解别只背八股要能推到源码边界并发这块的考题年年翻新但内核永远那几个。我挑五个出现频率最高的点每个都说清楚“该答到什么程度”才算过关。2.1 synchronized从锁升级讲到偏向锁的现实情况这道题几乎是必考但大多数人答得太“教科书”。标准答案是无锁→偏向锁→轻量级锁→重量级锁的升级过程以及每个状态下的开销对比。你需要补充的细节是偏向锁在JDK 15之后默认就是关闭状态因为偏向锁撤销的成本在当代高并发场景下经常比它节省的同步开销还高官方都标记为废弃倾向。所以你在面试时如果只复述“偏向锁获取时CAS写入线程ID”却不提这个默认关闭的事实遇到懂行的面试官就会被追问得很难受。更关键的你要能讲出轻量级锁自旋的边界。轻量级锁是通过CAS把对象头里的Mark Word替换成指向栈中锁记录的指针实现的自旋等待意味着CPU空转一旦竞争激烈超过自旋阈值就会升级成重量级锁底层依赖操作系统的互斥量线程挂起唤醒涉及用户态内核态切换。我面试时最喜欢追加的问题是什么业务场景下你会主动选择synchronized而不是ReentrantLock答案是简单的锁竞争、希望代码可读性强、并且能接受JVM自动优化的情况。你把升级路径和场景取舍讲清楚这道题才算答完。2.2 volatile可见性的底层是内存屏障不是“缓存失效”另一个高频陷阱是volatile的原理。很多人回答说“它强制线程每次都从主内存读所以保证可见性”这句话严格来说是不准确的。现代CPU都有自己的缓存volatile真正干的事是在指令层面插入内存屏障写操作时插入StoreStore屏障和StoreLoad屏障保证之前的普通写不会被重排序到volatile写之后同时让这个写对其他CPU核心可见读操作时插入LoadLoad屏障和LoadLoad屏障保证后续的普通读不会被重排序到volatile读之前。同时必须强调volatile不保证原子性。这是面试官百问不厌的坑——你写一个 i 在并发环境下照样丢更新因为i是读-改-写三步操作volatile只能保证每次读都拿到最新值但三步之间可能被其他线程插队。解释清楚这一点你顺便就能引出“所以需要AtomicInteger或者synchronized”的结论把答案自然过渡到CAS。2.3 AQS与ReentrantLockCLH队列变体为什么默认是非公平ReentrantLock的底层是AQSAQS的核心结构就三样一个volatile修饰的state、一个双向等待队列、以及基于LockSupport的阻塞唤醒机制。state在不同子类里有不同含义ReentrantLock里是重入次数Semaphore里是剩余许可数CountDownLatch里是倒计数。这些你得能串起来讲。面试官很爱问一个看似奇怪的问题非公平锁明明存在“插队”导致线程饿死的可能性为什么ReentrantLock默认还用非公平答案藏在性能里。公平锁每次获取锁都要先调用hasQueuedPredecessors检查队列里有没有排队的线程有就得乖乖排队线程上下文切换的频率更高非公平锁允许新来的线程先CAS抢一次抢不到再进队列这种设计在大部分场景下减少了阻塞唤醒的次数整体吞吐更高。挨饿是理论概率问题吞吐是现实收益问题JDK选择了后者。2.4 线程池七个参数之外的队列选择与拒绝策略线程池参数背下来不难难在给一个生产场景让候选人现场设计。核心线程数、最大线程数、存活时间、时间单位、工作队列、线程工厂、拒绝策略这七个参数背后的执行顺序才是考点任务来了先判断核心线程是否都在忙不在忙就新建核心线程执行核心线程忙就丢进队列排队队列满了再创建非核心线程直到最大线程数到极限了就触发拒绝策略。很多人不知道“先填队列、再扩线程”这个顺序其实这正是线程池跟普通连接池不一样的设计哲学它宁愿让任务排队也不轻易创建额外的线程因为线程的创建销毁和上下文切换成本很高。队列和拒绝策略的组合也经常出场景题Executors.newFixedThreadPool默认用无界LinkedBlockingQueue任务堆积时不拒绝但可能撑爆内存newCachedThreadPool用SynchronousQueue没有缓冲来一个任务就必须创建一个新线程瞬间高峰会创建出大量线程。生产环境我更推荐自定义核心线程数按CPU密集还是IO密集来估算CPU密集约等于核数1IO密集可以略高队列用有界队列容量根据业务接受度设拒绝策略用CallerRunsPolicy或者自定义告警策略前者让提交任务的线程自己执行起到天然限流的作用后者至少让线上问题有迹可循。2.5 ThreadLocal内存泄漏的锅弱引用只背一半ThreadLocal几乎是每场必问而且问法越来越刁钻。最常见的版本是既然ThreadLocalMap里的Entry继承了WeakReferencekey是弱引用那为什么还会内存泄漏正确答案特别反直觉弱引用反而加剧了问题——key被GC回收后变成null但value是强引用只要线程不销毁value就永远躺在ThreadLocalMap里导致内存泄漏。这才是真正需要remove的原因。另一个高频变体是线程池场景的脏数据问题。线程池里的线程是复用的如果你在某个任务里set了ThreadLocal却忘了remove下个任务复用这个线程时get到的就是上个任务残留的旧值。轻则数据串线重则带上用户的身份信息造成越权。所以我的建议很简单ThreadLocal用的地方一定配套try-finally remove养成肌肉记忆面试时把这两个坑都主动讲出来面试官会认为你真的踩过生产环境的雷。3. Spring高频题拆解三级缓存、AOP与事务失效是同一张考卷Spring这块的考点表面看很散实际上一大半都在围绕一个关键词“代理”。代理对象的生成时机、生成方式、失效场景串起了整个Spring核心。3.1 三级缓存为什么一定是三级两级行不行“Spring怎么解决循环依赖”这道题已经问烂了但每年还是有一大批人挂在追问上。一级缓存singletonObjects存的是完整Bean二级缓存earlySingletonObjects存的是提前暴露的早期Bean半成品但已经被依赖方拿走了三级缓存singletonFactories存的是ObjectFactory函数式工厂。核心流程是A创建时发现自己要注入B于是先把自己通过三级缓存的工厂暴露出去B创建时发现要注入A从三级缓存拿到A的工厂调用getEarlyBeanReference拿到早期引用注入给自己B完成初始化后A回头继续加工如果A需要AOP代理此时代理已经提前生成。那为什么不能用两级缓存关键在AOP。Spring默认的AOP代理是在Bean初始化完成之后通过BeanPostProcessor生成的如果循环依赖发生时只往二级缓存放一个“原始对象”依赖方拿走的就不是代理对象后面再生成代理就晚了。三级缓存里放的不是对象而是ObjectFactory就是为了把这个决策延迟到“被真正依赖”的那一刻——如果一个Bean虽然进了循环依赖但从未被提前引用那它就不需要提前生成代理完全可以等到正常生命周期末尾再代理。三级缓存的本质是让代理生成的时机从“一刀切”变成“按需触发”。3.2 AOP代理对象JDK动态代理和CGLIB怎么选这道题考的是你对代理机制的底层理解。规则很清晰目标类实现了接口Spring默认用JDK动态代理通过Proxy.newProxyInstance生成一个实现相同接口的代理类目标类没有接口就用CGLIB通过生成目标类的子类来覆盖方法。Spring Boot 2.x之后默认强制使用CGLIB即使有接口也用它目的是统一行为避免“明明有接口却被外部强转成具体类”的坑。但真正体现水平的地方是你能指出JDK代理的经典失效场景内部方法调用。比如一个类里方法A调方法BA和B都加了Transactional注解外层调用A时走的是代理对象A内部调用B时这个this指向的是目标对象而不是代理对象所以B的事务注解直接失效。同样的问题在Async、Cacheable上都会出现。你能主动把这个坑讲出来面试官就知道你真的写过被代理的代码而不是只看了文档。3.3 事务失效除了自调用还有四个高频场景事务失效是Spring面试的富矿一道题能挖出一串知识点。我总结最常见的五类场景同类方法自调用上面说过了本质是绕过了代理。private方法上加Transactional因为CGLIB/JDK代理都没法增强私有方法。异常被catch吞掉事务感知不到任何异常自然不回滚。抛出的是checked异常Spring默认只对RuntimeException和Error回滚需要显式指定rollbackForException.class。多线程调用Transactional是基于ThreadLocal持有数据库连接的你在子线程里调用的方法拿不到主线程的事务连接。面试时把这五个场景一口气列出来顺便每个补一句解决思路比如自调用用注入自身代理或TransactionTemplatechecked异常指定rollbackFor子线程要么把异常抛回主线程等待要么用编程式事务这道题基本就锁分了。这五个场景背后其实是一个统一原理Spring事务的本质是AOPAOP生效的前提是方法调用必须穿过代理对象。3.4 Spring Boot自动配置条件注解才是灵魂现在很少直接问Spring怎么用了问的都是Boot的自动配置原理。SpringBootApplication是个组合注解核心是EnableAutoConfiguration。它通过AutoConfigurationImportSelector去读取META-INF/spring.factories或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里的候选配置类列表然后逐个用Conditional系列注解做过滤。ConditionalOnClass判断classpath里有没有某个类ConditionalOnMissingBean判断容器里还没有某个BeanConditionalOnProperty判断配置项。面试加分点是你能答出“自己定义的Bean为什么能覆盖默认配置”。因为RedisAutoConfiguration里的RedisTemplate被标了ConditionalOnMissingBean你的配置类只要先被扫描到容器里已经有了一个RedisTemplate自动配置的那个就会被跳过。同理想排除某个自动配置可以用exclude属性或者excludeName。Spring Boot这一整套设计的目的就四个字约定优于配置但保留反悔的权利。4. MyBatis高频题拆解从占位符到二级缓存全是执行器的细节MyBatis看起来简单面试官偏偏爱在最简单的点上挖坑。这个模块的四道题每一道都有“你以为你懂其实你没懂透”的潜质。4.1 #{}和${}预编译的边界在哪里绝大多数人都能答出“#{}是预编译占位符${}是字符串拼接前者防SQL注入”但这个答案只能拿一半分。另一半在于说清楚底层使用#{}时MyBatis会把SQL解析成带有问号的PreparedStatement参数通过setXxx方法绑定数据库端预编译参数无论传什么都被当成值不会被拼接成SQL指令而${}是纯粹的字符串替换发生在SQL解析之前参数内容会原样进入SQL文本所以只要有用户可控内容进到${}里就存在注入风险。但反过来也要知道${}不是一无是处。动态表名、排序字段、以及一些必须动态拼接的SQL结构只能用${}。面试官问到这里通常会追加那怎么避免${}的注入风险答案是白名单校验比如表名和排序字段先在一个枚举或配置里映射一遍校验通过再拼进去。能答出白名单方案说明你确实在实战里处理过这个矛盾。4.2 一级缓存与二级缓存都在什么场景下失效MyBatis缓存题是个经典的反直觉考点。一级缓存是SqlSession级别的默认开启同一个SqlSession里执行两次相同查询第二次直接命中缓存。但这里有个巨大的坑Spring整合MyBatis之后如果你没有开启事务每次Mapper调用都会新建并销毁一个SqlSession一级缓存等于形同虚设。只有包在同一个事务里时Spring的SqlSessionTemplate才会复用同一个SqlSession一级缓存才真正起作用。很多人在网上看了一堆“一级缓存默认开启”的资料却不知道在Spring环境下的真实行为这个点你面试时主动说出来绝对是加分项。二级缓存是namespace级别的也就是一个Mapper.xml对应一个缓存区域多个SqlSession共享。它需要手动开启开启后要面对两个实际问题一是缓存对象需要序列化因为要防止多个会话拿到同一个对象引用互相干扰二是跨namespace的脏读问题。典型场景是A表的查询缓存命中后B表更新了A表关联的数据A的缓存根本不知道给你返回旧数据。所以生产环境我一般不推荐轻易开二级缓存除非这个Mapper的查询集中在单表、读多写少、对强一致没要求。4.3 插件机制为什么说MyBatis插件本质是一次“四大组件”的代理MyBatis插件是很多高级功能的地基分页插件、数据权限插件、慢SQL统计全是靠它做的。插件能拦截的对象有四个Executor、StatementHandler、ParameterHandler、ResultSetHandler。之所以能拦截是因为MyBatis创建这些对象时会检查有没有注册插件有的话就用JDK动态代理包一层。多个插件同时存在时先注册的先被包在里面一层一层套成洋葱结构执行时先走后注册的这就是插件的责任链模型。拿PageHelper举例它就是一个拦截Executor.query方法的插件先拿到即将执行的SQL用方言解析器改写成带limit的SQL再放行给JDBC执行。理解了这个原理别人问你“怎么实现一个多租户插件”你就能立刻给出思路拦截Executor.query方法修改BoundSql里的SQL把租户ID条件拼进去。面试官问插件这道题真正考的就是你能不能从“会用”迁移到“会设计”。4.4 手写MyBatis的核心链路“有没有手写过一个简单的MyBatis”这道题在大厂越来越高频本质是考你吃没吃透MyBatis的执行主流程。完整链路是Mapper接口被注册后MyBatis用JDK动态代理为它创建一个MapperProxy调用接口方法时代理类根据方法签名找到对应的MappedStatement接着解析SQL把#{}参数替换成占位符生成BoundSqlBoundSql交给Executor执行Executor负责看缓存、获取数据库连接真正操作JDBC的是StatementHandler它通过ParameterHandler设置参数通过ResultSetHandler把结果集映射成POJO。面试时你不需要真的把每行代码写出来但要把这个链路按顺序讲全。如果被追问“SQL是怎么映射到方法的”要能答出MappedStatement的key规则——namespace加方法名如果被追问“结果集是怎么映射的”要能说出MyBatis的自动映射策略以及Results注解的作用。这条链路你讲顺了相当于直接告诉面试官我读过MyBatis源码而且知道从哪一行开始读。5. 500道题不是用来背的备考分层与面试表达策略最后这部分聊聊这套题到底应该怎么用。我见过太多候选人把题库当成“背诵清单”一条一条背下来结果面试官换一个场景问就傻了。题库的价值是帮你划出知识点地图用得好不好取决于你有没有给它们分层。5.1 三轮分层高频基础、源码边界、场景题第一层是高频基础题比如锁升级、Bean生命周期、事务失效场景、MyBatis缓存这些要达到“闭眼能完整复述”的熟练度因为它们是后续所有追问的起点。第二层是源码边界题比如三级缓存为什么是三级、MyBatis插件如何实现拦截这些要能讲到关键类和关键方法级别能画出一条清晰的主链路。第三层是场景题比如“线上查询突然变慢你怎么排查”“多个服务并发调用怎么编排”“缓存和数据库一致性怎么保证”这类题目没有标准答案考的是你把前两层知识组合应用的能力。我给候选人的建议是前两层花七成时间按模块逐个击破第三层用模拟面试来练每周至少安排两场问自己“如果我是面试官我会揪着哪里追问”。这套题里的每个知识点你都要能顺着它往上下游各追问三层问不下去了就是你的边界回去翻源码补上。5.2 面试回答的结构结论先行加边界条件加场景验证答题方式也是能练出来的。我发现高分候选人普遍有一个共同点回答任何一道题都自带结构。第一句先给结论比如“Spring用三级缓存解决循环依赖核心思想是提前暴露早期Bean”第二句开始讲边界条件比如“但是只有singleton作用域的Bean才能解决循环依赖prototype作用域不行”第三句用场景验证比如“实际开发中最常见的循环依赖是两个Service互调加个Lazy也能绕过去”。这种结构的好处是面试官不用替你在碎片化信息里找重点追问起来也轻松。反过来低分答案的共同点是只背结论不给边界比如“解决循环依赖就是三级缓存”问“原型Bean怎么不失效”就卡壳。边界条件才是区分背题和理解的分水岭。5.3 我踩过的坑与建议最后说点个人的体会。我整理这套题最大的教训是别贪多。一开始我恨不得把所有可能的题全塞进去列到八百多道结果自己都刷不完了。后来砍到500道再给每道题标注了“核心原理”和“追问边界”效果反而好很多。你刷题也要有这个“砍”的意识——每道题不值得平均用力高频题要多花时间抠源码低频冷门题做到“知道答案能复述”就够了。还有一个很实用的建议把题目打印出来当口述目录用。每天抽出半小时随机抽十道题对着空气讲讲完再回听录音。你很快就发现脑子里觉得“会了”的题嘴上讲出来全是断点。能对着录音机讲顺的题在面试官面前才不会翻车。这个习惯我从去年坚持到现在比刷任何面试视频都管用。
返回列表