尧图精选

5年Java工程师面试题集:并发、JVM与生产排查实战提炼

🕒 发布时间:2026/10/1 4:58:42 📁 来源:尧图网络
做过几年Java面试官也经历过几次跳槽被面试。发现网上大部分面试题集要么停留在校招难度要么堆了一堆“背诵全文”式八股文真正针对5年以上经验的题目反而不成体系。这套题集是我结合近年面试别人和被别人面的经验整理出来的覆盖并发、JVM、框架工具、设计能力和生产事故排查五个维度每一道题都附了答题思路和考官追问方向。适合准备跳槽的Java工程师、想带新人的技术组长以及需要出面试题的面试官参考。1. Java基础与并发编程面试要点1.1 AQS背源码之前的三个理解层次AQSAbstractQueuedSynchronizer是Java并发包的基石ReentrantLock、Semaphore、CountDownLatch全部建立在这套机制上。5年经验的面试里AQS不是可选题而是必选题。但多数候选人只会背“内部维护了volatile int state和一个FIFO队列”这句话考官一追问“为什么要用双向队列”就直接卡壳。回答AQS建议按三个层次递进。第一层讲整体架构state表示同步状态通过CAS修改获取锁失败的线程包装成Node进入同步队列在队列中自旋或阻塞等待被唤醒。第二层讲关键细节为什么队列是双向的因为park/unpark唤醒的是头节点的后继节点而锁释放时要把后继节点状态从SIGNAL改成0再执行unpark如果只有单向链表后继节点无法反向找到自己的前驱来修改状态。为什么state必须用volatile因为多线程都要读取最新值用synchronized太重volatile配合CAS就能实现无锁更新。第三层结合具体实现讲ReentrantLock的非公平锁在acquire前先尝试一次CAS抢锁抢不到才走队列所以新线程可能比队列中排队的线程先拿到锁Semaphore用共享模式释放资源时要循环唤醒后继节点。提示面试时如果手边有纸一定画一遍acquire流程——先tryAcquire失败则addWaiter入队然后acquireQueued自旋或阻塞。画图能直观展示你对队列操作的理解也会给考官留下“真懂源码”的印象。1.2 数据一致性从库存扣减这道送命题说起“下单减库存怎么保证不超卖”是出现频率最高的数据一致性场景题。我第一次被问到这道题时脱口而出“先查库存再判断大于零就update”面试官当场让我回去等通知。后来才明白这道题想考察的是你对并发控制手段的掌握边界。正确的解题思路分三步逐步递进。第一层数据库原子更新update stock set count count - #{num} where id #{id} and count #{num}这条SQL用行锁配合条件判断实现原子扣减100个并发请求同时扣10个库存数据库行锁会串行执行count #{num}保证最后一批请求更新行数为0。第二层乐观锁在表里加version字段update stock set count count - #{num}, version version 1 where id #{id} and version #{version}更新前compareAndSet失败就重试。第三层分布式锁Redis的SETNX或Redisson的RLock锁住商品维度再执行扣减适合库存记录不在同一数据库的场景。面试官接下来会追问主库和缓存Redis之间的数据一致性怎么保证因为一般高并发场景会先读缓存再异步同步到数据库。这里要答出本地消息表、MQ事务消息、订阅binlog如Canal三种方案重点说明最终一致性和“什么时候能容忍短暂不一致”的业务判断而不是机械背方案。踩过的坑也值得分享事务一定不能加在锁里面否则锁内执行提交前全程持有数据库连接连接池很快耗尽分布式锁一定要设置过期时间还要考虑续期策略Redisson的看门狗能自动续30秒但也要防住断网后锁没释放的极端情况。1.3 并发工具套件的延伸追问问完AQS考官通常还会从并发工具集里抽几个来验证实际使用能力。高频题目包括ConcurrentHashMap的size()方法为什么Java 8重写了CountDownLatch和CyclicBarrier的区别线程池的拒绝策略怎么选ThreadLocal的内存泄漏是怎么发生的。回答线程池时要有数据概念核心线程数怎么设置IO密集型任务通常设置CPU核数 * 2CPU密集型设置CPU核数 1。更重要的是答出“阻塞队列选型”的思路——LinkedBlockingQueue无界会撑爆内存SynchronousQueue不存储任务会立即触发拒绝策略有界队列ArrayBlockingQueue配合理想的拒绝策略是最常用方案。ThreadLocal这道题关键讲两件事ThreadLocalMap的Entry里key是弱引用、value是强引用线程池复用线程导致value一直有引用链所以用完之后要主动调用remove()。如果只答“底层是个Map”考官会认为你2年经验都没有。2. JVM与内存管理高频题2.1 双亲委派是入场券真正拉开差距的是打破机制类加载机制几乎是JVM考区的必问项。类加载的五个阶段要能按顺序背出来并解释每个阶段做什么加载、验证、准备、解析、初始化。准备阶段要强调“静态变量赋的是零值初始值在clinit阶段赋值”这个细节很多人栽在这里。双亲委派的价值要答两点避免核心类库被自定义类覆盖保证java.lang.String永远是启动类加载器加载的那一份保证类的唯一性同一个类被不同类加载器加载会产生不同Class对象。答完这些至少一半候选人会停止但5年经验想拿高分必须主动聊“怎么打破双亲委派”和“为什么需要打破”。结合工作的例子最加分Tomcat为每个Web应用创建独立类加载器应用间jar包隔离互不影响JDBC的DriverManager用SPI机制反向加载驱动实现类因为DriverManager是启动类加载器加载的双亲委派无法找到第三方驱动只能通过ServiceLoader反向加载。热部署的本质也是自定义类加载器卸载旧版本类。2.2 对象在堆里怎么活从内存结构聊到GC日志JVM内存模型这道题的踩分点在于不要只背堆、栈、方法区三个名词要说明谁在用这些内存、对象怎么流动。新对象分配在Eden区Minor GC后存活对象进入S0或S1每熬过一次GC年龄加1达到阈值默认15晋升老年代。大对象直接进入老年代可以设置-XX:PretenureSizeThreshold参数。面试官最爱问“线上老年代增长怎么办”实际的排查链路是先用jstat -gcutil看GC频率和内存占用趋势再用jmap -dump导出堆快照用MAT分析大对象和引用链。我遇到过一个经典案例一个定时任务每次执行都往静态Map放一个ArrayList任务结束后Map不清空排查时发现Map里锁了90%的老年代空间。所以答题时要提到jstack看线程栈找定位通过jstat看Full GC前后的内存差异第二点就要有排查思路和工具组合。CMS的退化问题也常被追问。答出一点就算过关CMS在并发标记和并发清理阶段会和业务线程并行内存碎片多时会退化成Serial Old进行Full GC导致STW时间暴增几十倍。追问触发条件要答出老年代碎片过多、concurrent mode failure、晋升失败三种情况。2.3 Lombok报错的快速定位编译器版本问题其实有套路开发中经常遇到这行报错java: You arent using a compiler supported by lombok, so lombok will not work。看到这行字不要慌本质是编译环境变了但Lombok没跟上。排查顺序按三步走先看JDK编译版本用mvn -version和javac -version确认再看Lombok依赖版本旧版本比如1.16系在JDK 9以上基本不可用需要升到1.18.x最后看IDE里的注解处理器选项是否被误关闭IDEA的Settings - Build - Compiler - Annotation Processors要勾选Enable annotation processingMaven编译则确认有maven-compiler-plugin且版本和source/target一致。真实项目里还遇到过更隐蔽的情况同一模块依赖多个Lombok版本传递依赖把旧版本带进来了。所以排查时果断用mvn dependency:tree -Dincludesorg.projectlombok:lombok看一眼实际生效的版本往往一眼就能找到元凶。3. 框架与工具链实战环节3.1 行级权限控制纸上谈兵容易落地才是硬功夫行级权限几乎是每个业务系统都要做的功能但5年经验的面试官问起来不是让你把RBAC里的角色、用户、权限表名称背一遍而是要你给出一个实际可落地的数据权限控制方案。最常见需求是销售只能看到自己的客户部门经理能看到本部门客户管理员能看到全部客户。推荐的技术方案是注解 AOP SQL条件拼接。定义DataScope注解加在Service方法上声明数据权限维度切面里从当前登录用户上下文取出角色权限配置解析后生成WHERE条件比如WHERE customer.owner_id #{userId}再通过ThreadLocal传给MyBatis的拦截器拼接到SQL末尾。这个方案解决了业务代码里到处写if (isManager) ... else ...的痛点权限规则集中管理新角色接入只改配置。面试追问通常会集中在两个点上一是怎么防SQL注入动态拼接的条件值必须用参数绑定而不是字符串拼接二是大表权限过滤后的分页性能建议强制走索引销售维度的owner_id必须有索引同时用角色缓存减少重复查询。如果你主动补充“数据权限和敏感字段脱敏要配合”考官会眼前一亮。3.2 Java POI Word到底能不能生成图表一个常被误解的功能网上有个高频搜索问题Java POI Word能生成图表吗答案是能但说“能”的人大多没真正做过。POI对Word图表支持较弱至今没有官方稳定API直接插入Chart对象.XLSX的图表反而友好一些。实际做法有两种。第一种模板法用WPS或Office先手工做好一个带图表的.docx文档作为模板里面定义占位数据然后用POI加载模板通过XWPFChart遍历图表对象替换category和series的数据点。这个方案对POI的版本要求高需要用4.1.0以上的较新版本对图表类型支持也有限折线图、柱状图能搞定饼图偶发失败。第二种图片法用JFreeChart或ECharts生成图表图片再调用XWPFParagraph.run.addPicture()把图片插到Word里。图片法可控性高任何图表类型都能做唯一缺点是图表数据无法在Word里继续编辑但大部分业务报表场景根本不要求编辑。我参与的报表系统最终选了图片法原因很简单稳定压倒一切线上跑了两年的生产报表没有因为图表生成出过问题。面试时把这两种思路讲清楚再说明“图表是用图片还是原生对象要看业务是否需要二次编辑”比直接背API强十倍。3.3 环境配置与版本坑排雷一次类工具问题集中处理5年经验的工程师在生产环境配过环境变量的机会不少这块也会出现在面试的机器环境类问题里。Win11系统配置Java环境最容易踩的坑是系统变量和用户变量都设置了JAVA_HOME且指向不同JDK版本最后s报错。配置建议讲三点JAVA_HOME统一指到JDK安装根目录不是bin目录Path里添加%JAVA_HOME%\bincmd里验证java -version和javac -version打印版本一致。有一类冷门又实际的问题某些命令行工具启动时报找不到Java比如Drozer、Minecraft的PCL启动器这类依赖JRE的工具。原因通常是JRE和JDK路径不一致、安装的是JRE而不是JDK新版JDK安装时未勾选JRE组件、环境变量PATH被其他版本抢先指向。排查清单先which java或where java看当前用哪个再echo %JAVA_HOME%确认路径最后检查工具自身配置里的Java路径是不是硬编码了不存在的目录。Java 8版本段的知识也值得掌握Oracle的JDK 8u201之后是商业付费版本很多公司生产环境停留在8u201之前的版本这就是Java 8 u201这个数字频繁出现在排查帖里的原因。面试时主动说出“我们在8u202之后不得不评估OpenJDK或更换发行版”这句话说明你对版本演进和许可风险确实有认知。4. 设计模式与编码能力考察4.1 面向对象和设计模式别只会背“单例的懒汉饿汉”大多数候选人答设计模式题都停留在把单例的几种写法背一遍的程度。5年经验的考察早就不一样了考官真正想听的是设计原则在实际业务里的落地而设计模式只是实现原则的工具。我面试时喜欢出这道题假设要对接微信、支付宝、银联三种支付渠道支付逻辑各自不同后续还可能增加新渠道你怎么设计最佳回答是定义PayHandler接口包含pay()、refund()、query()三个方法三种渠道各写一个实现类并注册为Spring Bean。再利用Spring的依赖注入特性用一个MapString, PayHandler批量注入所有处理器payChannel作为Key。调用方只依赖接口和Map新增渠道不需要改老代码这就是策略模式加工厂模式组合。考官继续问“Map里的处理器是什么时候初始化的”需要答出Spring在单例Bean实例化完成后会处理Autowired注入把容器中所有PayHandler类型的Bean收集成Map。追问“渠道之间怎么隔离”要补充每个渠道自己的配置对象、超时时间、重试策略都独立维护避免互相影响。这样一套答下来比抱着一本《设计模式》背十遍都管用。4.2 排序算法在面试和工程里的真实位置Java里日常开发排序基本都是Collections.sort()或Arrays.sort()内部实现是DualPivotQuicksort和TimSort算法题里手写几种排序仍然保留位置。问冒泡排序不是要你表演背诵而是考察边界处理和复杂度意识。面试时手写冒泡要注意外层循环i length - 1内层循环j length - 1 - i每次内层结束后最大的数沉到末尾。写好之后考官一定会追问“能不能优化”。一种是增加标志位某一轮没有产生交换就提前结束另一种是记录最后交换的位置下轮比较只到那个坐标。能答出优化再引出快排的O(n log n)平均复杂度和TimSort对工业级排序的优化思路利用已有序片段合并面试效果会好很多。工程里的排序更关键要讲稳定性。Arrays.sort()对对象数组用TimSort是稳定排序对基本类型用DualPivotQuicksort是不稳定排序。如果实现自定义对象CompareTo时返回0表示相等TreeSet和HashMap的equals/compareTo语义混用会出诡异Bug。这个细节能答出秒杀只会背API的程序员。4.3 Lambda和Stream内部类、作用域与常见翻车点5年经验的Java工程师手写Lambda已是常态但真被问到底层原理还是有人懵。Lambda表达式本质是语法糖编译阶段通过invokedynamic生成实现函数接口的匿名类。一个常考的问题是Lambda为什么不能修改外部局部变量原因在于局部变量存储在栈上匿名类运行在捕获环境下需要复制一份复制后对原变量的修改无法生效如果变量在初始化后不再变更就能安全捕获。effectively final是JDK 8引入的约束变量值未改变即视为final。内部类方面考官会现场抛一道题“写一个 Lambda 捕获内部类实例字段和静态字段的示例并解释区别”。实例字段和静态字段所在对象有独立生命周期可以随意修改局部变量不行。要结合源码级别说明运行时生成的匿名类里会保存捕获局部变量的副本字段构造器接收外部传入值。IntStream.range(1, 100).forEach(i - System.out.println(holder.count))能打印递增计数换成局部变量int count 0就无法声言再次赋值了。Stream的实际考察重点在翻车点。同一个Stream用两次会抛IllegalStateExceptionparallelStream处理ArrayList统计元素个数必须用线程安全的方式或collectmap操作里做缩略包装类型拼接很容易触发空指针count()返回long类型直接给SQL分页参数int会溢出。这些是基于实战的补充细节工作年限越长越要答得出来。4.4 对象深拷贝的三种实现与性能对比深浅拷贝是集合和对象操作里的高频题。浅拷贝只复制基本类型字段和引用地址obj2.setList(obj1.getList())两个对象共用一个List改一个另一个跟着变。深拷贝需要连引用指向的对象一块复制。三种实现各需口述利弊重写Object.clone()类继承Cloneable接口逐个复制嵌套对象最繁琐且侵入性强序列化方式先实现SerializableByteArrayOutputStream写出再反序列化回来写起来少但性能差、要求全链路对象都可序列化加个transient字段略过会丢数据JSON序列化用Jackson或Gson把对象toJson再转回对象性能和安全性介于两者之间嵌套结构天然支持但引用了无法序列化的字段会报错。工程经验补充一条别在代码里到处做深拷贝优先设计不可变对象通过Builder构造新对象配合Lombok的Value能避免一大类引用共享问题。5. 高频实战问题与排查技巧实录5.1 数组越界异常生产环境最常见的低级又隐蔽的错误ArrayIndexOutOfBoundsException在面试里也要当回事因为很多生产事故表面看是并发、性能最后定位出来就是一个越界。经典场景是for循环中边遍历边remove元素ArrayList内部数组被改变后索引错位轻则越界重则漏删。规范做法是用迭代器的iterator.remove()。另一个典型案例是分页查询改造并行流后数组索引越界。原逻辑用普通for循环对pageList按下标处理改成parallelStream().forEach(i - handle(pageList.get(i)))后ArrayList线程不安全多个线程同时扩容或结构变更导致越界。排查思路就很清楚先复现现场然后看调用栈定位到forEach和get方法接着检查集合的线程安全属性。生产事故有一个经验越界异常比空指针好查因为栈信息里有行号但人都容易只看“数组越界”四个字就忽略行号越过最直接线索去猜原因。正确顺序永远是先看栈顶行号找到是哪个集合哪个索引再讨论并发和数据结构问题。5.2 数据类型与编码细节的送命题Java基础里最容易被5年经验选手翻车的是包装类型比较和String机制。Integer a 127, b 127; a b是true因为常量缓存池覆盖-128到127改成128就是false。这个机制源码级别要看IntegerCache.low和IntegerCache.high两个常量high一般等于127可以通过-XX:AutoBoxCacheMax调整。String的不可变性要答出三方面含义substring和concat等方法都返回新对象不修改原串不可变才能保证String常量池安全多个引用共享同一个实例不会互相干扰String能作为HashMap的Key是因为hashCode被缓存且不变化这一点很多候选人答不到。编码题喜欢结合场景“读文件时中文变成问号怎么办”。底层原因是数据流解码用的字符集和文件实际编码不一致UTF-8文件被用GBK解析就会出现乱码。用InputStreamReader(stream, StandardCharsets.UTF_8)显式指定字符集不依赖平台默认值。我在实际项目中还遇到过一种隐蔽情况HTTP接口返回编码正确但日志文件编码和服务器系统编码不一致导致排查压力大最后统一约定UTF-8才消停。回答编码问题能带出“全链路统一字符集”意识这在团队协作里很加分。5.3 面试题集使用建议从背题到答题的能力切换刷题不等于面试成功分享一套我面试别人时的观察标准也是自己准备面试时用得上的方法。第一层是“背题”能完整复述答案但经不住追问对应的年限约1-2年。第二层是“做题”能结合项目经历讲述某个技术的落地过程对应的年限3-5年。第三层是“讲题”能在讲过程中把问题引向自己擅长的领域并主动抛出深层次的关联问题这是5年以上经验该有的状态。准备期间最好把自己实际解决过的线上问题整理成案例用标准的“背景-方案-效果-反思”四段式写进笔记。面试官问“你遇到最难的Bug是什么”时直接抛出真实案例远比编造有杀伤力。例如我处理过一个消息积压问题问题根因不是消费者性能而是消费线程里调用一个超时返回的第三方API每次等待8秒导致堆积。排查过程是生产日志上看到线程池活跃度低再用jstack发现大量线程卡在SocketRead最后调整策略把异步调用改成同步加超时。这样的故事才是5年经验的正面证明。最后再分享一个技巧每次面试结束后把被问住的问题和面试官的追问逻辑记录下来存到自己的题目库里。被问住的地方往往就是现有知识体系最薄弱的地方补齐这些点半年后你的面试通过率会出现明显变化。面试本质上不是对知识的复述还是对工程经验的结构化表达积累得足够多表现自然就稳了。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →