☰
Java 5年经验面试题集:从JVM到分布式,深度与广度全解析
2026/10/1 3:31:39 网站建设 项目流程

我最近帮几个团队做模拟面试,发现一个挺有意思的现象:简历上写着“5年Java开发经验”的候选人,一聊到锁升级、AQS、分布式事务这种题,很多人就明显接不住了。倒不是说他们代码写得差,而是这些年一直在业务需求里打转,底层原理和架构取舍反而成了软肋。这篇《Java开发工程师(5年以上经验)面试题集》不是让你背题用的,而是想帮你画一条线:5年这个节点,面试官到底在考察什么,哪些题必须答出深度,哪些地方最容易翻车。

我整理了工作中真实高频出现的面试题,按模块拆开讲,每道题都附上回答思路和容易踩的坑。

1. 5年经验的Java面试,和初中级到底差在哪里

先别急着看题,捋清一个前提:5年经验的面试,考察逻辑和1-3年完全不同。应届生和初级开发者,面试官看的是基础扎不扎实、CRUD能不能扛住;到了5年,很多团队是按“技术骨干”“项目Owner”的预期来招人的,也就是说,你不但要能写代码,还要能扛模块、能排查线上故障、能在技术选型的时候说出为什么。

我面过不少人,资历写在纸上都很好看,但一开口就露馅了。常见的表现有两种:一是深度停留在“会用”层面,比如知道synchronized可以加锁,但问“偏向锁和轻量级锁的区别”就卡住;二是广度不够,只熟悉自己那一亩三分地,问分布式锁、消息队列、缓存一致性,完全没概念。

5年经验面试的分水岭,我认为体现在五个维度:

维度初级/中级5年以上
知识深度知道API和用法理解原理、源码、设计动机
故障排查能定位明显Bug能从现象反推链路,定位根因
性能优化会用工具能从JVM、SQL、架构三层做取舍
分布式听说过概念能说清一致性、幂等、CAP取舍
技术选型别人用什么用什么能对比方案,结合业务给结论

所以这篇文章的题目虽然是“面试题集”,但本质是帮你补上“深度”和“广度”这两块短板。下面的题,都是我实际问过、也被别人问过的高频题目,有些看着基础,但是往深了挖,每一道都能聊二十分钟。

2. JVM与内存调优:这几个问题答不好,后面基本不用面了

JVM是5年经验面试的“送分题”也是“送命题”。送分是因为考点高度固定,JMM、类加载、GC、OOM,翻来覆去就这些;送命是因为大家都背过概念,但一追问细节、一给场景,很多人就变成了背书机器。

2.1 线上CPU飙高,你怎么排查

这是线上故障最典型的场景。候选人有没有实战经验,这道题一测就知道。标准链路应该是:

  1. top -Hp看哪个线程CPU高,记下线程号;
  2. jstack导出线程栈,把线程号转成十六进制,在栈文件里搜;
  3. 如果卡在GC线程,大概率是Full GC频繁,再jstat -gcutil看GC频率和耗时;
  4. 如果卡在业务线程,看是锁等待还是死循环,锁等待会停在park或wait,死循环会停在某个方法调用上。

很多5年候选人能走到第二步,但第三步就断了。他们知道用jstack,却不知道配合jstat看GC,或者不会用jmap导堆转储。这里有个我踩过的坑:jstack刚导出来的时候线程栈是不准的,因为采样是瞬时的,一个线程可能在几毫秒内从IO切到CPU密集,所以最好多导几次,间隔几秒,对比差异才靠谱。

更深一层的问法是:GC线程为什么CPU高?这背后可能是内存分配过快、晋升阈值设置不合理、或者有大量短期对象。顺着这条线,面试官想听的是你能从“看到现象”到“定位根因”的完整链路。

2.2 内存区域与OOM的对应关系

OOM是个大帽子,但5年经验的人不能只说“内存不够了”,要能区分是哪块区域出的问题:

  • 堆内存OOM:最常见,java.lang.OutOfMemoryError: Java heap space。通常是大集合没释放、或者批量加载了过多数据。反过来问一句:堆OOM能不能靠调大Xmx解决?不能,那只是推迟崩溃时间,必须先找到谁在占内存。
  • 元空间OOM:Metaspace,常见于动态生成类(CGLIB代理、热部署)。很多人在堆上查了半天,其实是元空间爆了。
  • 栈溢出:StackOverflowError不是OOM但更常见,递归没出口、或者调用链太深。
  • 直接内存OOM:Direct buffer memory,NIO用多了没释放。这个最坑,默认MaxDirectMemorySize等于Xmx,但它不归GC管,要盯着。

每种OOM的排查手段不一样。堆OOM用jmap -dump导堆再结合MAT分析;元空间OOM看加载了哪些类、谁在反射生成类;直接内存OOM看Netty或RPC框架的ByteBuffer池。

2.3 双亲委派,为什么非要这么设计

这也是高频题。双亲委派的核心动机是防止Java核心类库被篡改。如果每个ClassLoader都自己加载java.lang.String,那类库有多份,类型系统就乱了,同一个类在不同ClassLoader眼里是不同类型。

面试官通常会追加一个问题:怎么打破双亲委派?有三次机会:一是Thread.currentThread().getContextClassLoader(),典型的是JDBC驱动,SPI机制需要反过来让父加载器加载子类路径的类;二是重写findClass(这是扩展,不算真正打破);三是重写loadClass,真正打破父优先策略,热部署框架Tomcat就是这样的。

说个细节,很多人不知道JDK9之后双亲委派从“继承优先”变成了“模块优先”,ClassLoader的源码注释里明说了要覆写findClass而不是重写loadClass。面试时能补一句这个演进,观感会好很多。

2.4 G1还是ZGC,选型依据是什么

5年经验最好别停留在“G1分Region,能预测停顿”这种层面。要能说出实际场景下的取舍:

  • G1:JDK9之后默认,目标是“在可控停顿下兼顾吞吐”,适合堆大小在几十GB以内、响应时间要求几毫秒到几十毫秒的业务。Real-world里90%的Java服务用G1就够了。
  • ZGC:目标是把停顿压到10ms以下甚至几毫秒,通过染色指针和读屏障做并发标记和移动,适合超大堆(上百GB)、对RT极其敏感的场景,比如证券交易柜台。
  • JDK21的ZGC已经支持分代收集,吞吐量提升明显,但默认还是G1。

面试官更想听的其实是:你线上用什么,为什么?比如“我们用的是G1,-XX:MaxGCPauseMillis=50,然后观察实际的P99 GC停顿,发现大对象分配频繁,所以把-XX:G1HeapRegionSize调到了16MB”。这种话一念出来,面试官就知道你调过参、看过监控,不是背八股。

3. 并发编程深水区:不再问synchronized,而是问锁升级与AQS底层

并发模块是5年面试拉开差距的核心区域。初级问“volatile和synchronized区别”,中级问“ConcurrentHashMap结构”,5年就要问“AQS的CLH队列到底怎么工作”“锁升级的临界条件是什么”。

3.1 synchronized锁升级的全过程

synchronized在JDK6之后是“先偏向、再轻量、后重量”的升级路径,这个是基础。但5年面试会追问:

  • 偏向锁撤销条件:多个线程竞争、以及调用hashCode时,偏向锁会撤销。我实际排查过一次诡异现象,代码里明明没有竞争,但性能就是上不去,最后发现是代码里调了System.identityHashCode,直接导致偏向锁批量撤销,JDK15之后偏向锁直接废弃,就和这个设计有关系。
  • 轻量级锁的自旋:轻量级锁靠CAS抢,抢不到就自旋,自旋超过阈值(-XX:PreBlockSpin默认10次)升级成重量级。但实际上自适应自旋会动态调整次数,JDK里这个参数已经被忽略了。
  • 重量级锁的等待队列:重量级锁背后是ObjectMonitor,有Entry Set和Wait Set两条队列。网上很多文章把这两条队列混为一谈,其实wait()和notify()走的是Wait Set,没抢到锁的线程在Entry Set,谁先拿锁由操作系统调度决定,没有严格的“先来后到”。

3.2 ReentrantLock为什么不用synchronized的monitor

ReentrantLock的核心是AQS。5年面试必问“讲一讲AQS的实现”,这道题能看出你是背过面试题还是真读过源码。

AQS的骨架就是一个volatile int state加一个CLH变体队列。以ReentrantLock的lock()为例,核心路径是:

  1. compareAndSetState(0, 1)尝试快速获取锁;
  2. 失败就addWaiter(Node.EXCLUSIVE)把当前线程封成Node,CAS挂到队列尾部;
  3. acquireQueued里循环:如果前驱是head,再次tryAcquire;否则shouldParkAfterFailedAcquire检查前驱状态,并LockSupport.park挂起线程;
  4. 前驱节点释放锁之后,unparkSuccessor唤醒后继节点。

很多人能背到第3步,但问“Node的状态字段waitStatus有几种”,就说不全。答案是CANCELLED=1、SIGNAL=-1、CONDITION=-2、PROPAGATE=-3,初始0。其中的SIGNAL表示“我释放的时候需要唤醒你”,理解了这个,AQS的链表唤醒逻辑就通了。

再追问公平锁和非公平锁的区别:非公平锁在lock()时先CAS一把,抢不到才进队列;公平锁直接进队列,hasQueuedPredecessors()检查前面还有没有排队的。实际业务里非公平锁吞吐更高,因为减少了线程挂起/唤醒的上下文切换,但可能出现“插队”导致等待线程饿肚子。

3.3 ThreadLocal的内存泄漏,被迫问出来算我输

ThreadLocal是并发模块里最容易被问爆的题,因为它和内存泄漏绑定在一起,很能测候选项到底是“背答案”还是“真踩过坑”。

泄漏原因:每个Thread内部有ThreadLocalMap,key是弱引用ThreadLocal,value是强引用。ThreadLocal被GC回收后(比如线程池里的线程还活着但ThreadLocal对象已经没引用了),key变成null,value却清不掉,形成了以null为key的Entry。

解决方式不是“用完remove就行”这么简单。我见过的务实做法:

  • 在finally块里remove(),这是底线;
  • 设计上让ThreadLocal存的对象是轻量级的(不要存大对象、不要存HttpServletRequest这种有上下文引用的);
  • 线程池场景下尤其注意,线程复用导致“脏数据”,比如上个请求的用户信息被下一个请求读到,这比泄漏更可怕。

面试时能说出“我用ThreadLocal存traceId,但发现线程池复用后traceId串了,后来每次请求进来先set、finally里remove”这种真实场景,比背十遍“弱引用强引用”都有说服力。

4. 分布式与数据一致性:5年经验的必战场

到了5年,分布式几乎是简历标配。哪怕你写的是“单体系统”,面试官也会问“如果拆成微服务,数据一致性怎么保证”。这块答不好,前面JVM聊得再好也悬。

4.1 分布式锁:Redis SETNX够不够用

这道题是绝对的“分水岭题”。思路清晰的人会立刻分出三个层次:

第一层,基础用法:SET key value NX PX 30000。注意一定要“NX+PX”一起用,分开两步执行就是坑——先SETNX成功再EXPIRE,中间进程挂了锁就永远不释放。

第二层,释放锁的原子性:释放锁得先把value(唯一标识)拿出来比对,是自己才删。这个“比对+删除”不是原子的,要用Lua脚本。Redisson的unlock()内部就是这么做的,一段Lua:先GET比对,再DEL。

第三层,主从切换锁丢失:在Redis主从架构下,客户端A在主节点拿到锁,主节点挂了,从节点接管后没有这把锁,客户端B又能拿到锁了。RedLock就是为解决这个问题提出的,但它在工程上争议很大。面试时能说出这个缺陷,然后补一句“Redisson的watchdog虽然能续期,但主从切换的窗口依然存在,所以极重要的场景里我倾向用Zookeeper的临时顺序节点+watch机制”,印象分会很高。

4.2 TCC和本地消息表,怎么选

数据一致性是5年经验的核心考题。可以这样说,没有分布式的项目也要问一遍“如果引入分布式你怎么做”,原因很简单:面试官想知道你有没有全局观。

2PC的协调者单点和阻塞问题太明显,现在问得少。TCC是Try、Confirm、Cancel三段式,适合对延迟敏感、业务愿意为一致性写补偿逻辑的场景,比如库存扣减。但TCC的难点不在框架,而在于空回滚、悬挂、幂等这三个工程细节,绝大多数候选人都答不上来。

本地消息表是“最终一致性”的最稳方案:业务操作和写消息放进同一个本地事务,然后异步扫表投递消息到MQ。它的优点是实现简单、可靠,缺点是消息表本身成为瓶颈、实时性差一点。现在很多团队用同库发MQ(比如RockeMQ事务消息)替代了手动建消息表。

面试官其实不期待你写代码,而是想听你做“取舍”:什么场景允许弱一致?什么时候必须强一致?比如订单创建后发短信,最终一致完全够;但转账余额扣减,你肯定不想出现“A扣了B没加”的情况。

4.3 幂等设计的三板斧

分布式题里“幂等设计”是必问的,而且特别务实。记住三个工具:

  1. 唯一键约束:数据库唯一索引兜底,比如支付订单号;
  2. 状态机:订单状态流转只能“已支付→已发货→已签收”,重复请求打到已签收就直接拒绝;
  3. Token预发放:前端先拿一个幂等Token,后端把Token存Redis,处理请求时先DEL,DEL成功才处理,DEL失败说明重复提交。

能再多说一句“幂等键的生成规则要考虑业务语义”就更好了。比如“用户ID+订单号+操作类型”做Hash,而不是全局用一个UUID——UUID虽然唯一但没法定位业务,遇到问题排查效率极低。

4.4 Seata的AT模式,用起来真没那么简单

不少团队引入Seata,但面试是考理解的。AT模式是基于“全局锁+undo_log”的改进版2PC:业务SQL执行前记录undo_log,执行后把数据快照和全局锁信息同步给TC;事务结束时删除undo_log,回滚时借助undo_log反向补偿。

有一个使用细节很多人踩过:AT模式对SQL有要求,比如UPDATE必须走主键或索引,否则会拿锁的范围变大,并发性能直线下降。我在项目里见过因为一条联表UPDATE导致整个分布式事务的性能从几百TPS掉到几十TPS的,后来改成拆两步操作,才把数据恢复过来。面试时能说出这种“性能陷阱”和“解决方案”,明显比“我们有用Seata”更有分量。

4.5 CAP和BASE,少说理论,多说在你项目里的取舍

“CAP不可能三角”这种题,5年经验的人不但要能说理论,还要能结合项目谈。我的回答模板供参考:

我们交易链路要求强一致,所以用了分布式事务保证ACID,但意味着可用性会降一点;我们报表链路能接受十分钟延迟,所以用的异步MQ加对账任务,做到最终一致就可以。

接着他会问“那你们怎么对账”,你就可以展开:对账任务每天凌晨跑,比对本地库和支付回调的状态,发现不一致的捞出来重新处理,或者发告警人工介入。这就是BASE思想在工程里的落地。

5. 框架与中间件源码题:看没看过源码,一问便知

5年经验面试,Spring、MySQL、Redis、Kafka这四样基本跑不掉。不是考你背API,是考“你知不知道它内部是怎么跑的”。

5.1 Spring Bean生命周期,说全11步

Bean生命周期是Spring模块的“必考送分题”,但很多5年的人也说不出全流程。标准答案可以这样组织:

  1. 实例化(构造器或工厂方法);
  2. 属性填充(populateBean,按名字或类型注入);
  3. Aware接口回调(BeanNameAware、BeanFactoryAware、ApplicationContextAware);
  4. BeanPostProcessor.beforeInitialization;
  5. @PostConstruct;
  6. InitializingBean.afterPropertiesSet;
  7. 自定义init-method;
  8. BeanPostProcessor.afterInitialization;
  9. 如果处于使用中,正常被调用;
  10. @PreDestroy;
  11. DisposableBean.destroy+ 自定义destroy-method。

面试官常追加的问题是“你项目里用过BeanPostProcessor做什么”。我建议准备一个真实案例,比如“我们写了一个日志增强的PostProcessor,扫描带@TraceLog注解的方法,自动生成AOP切面,避免每个接口手动写日志”,这一句话就能证明你能把源码知识用在工程上。

5.2 MySQL索引:为什么B+树,为什么最左前缀

MySQL索引题5年经验必须答出“为什么”:

  • 为什么不用B树:B树的非叶子节点也存数据,意味着同样大小的Page里存得更少,树更高、IO更多;
  • 为什么不用Hash:Hash适合等值查询但不适合范围查询,而SQL里BETWEEN、ORDER BY、LIKE 'abc%'都有范围语义;
  • 为什么最左前缀:联合索引是“先按第一列排,再按第二列排”,所以跳过第一列相当于索引整体失效。比如(a, b, c)索引,where b=1 and c=2用不上,但where a=1 and c=2能用到a那一部分,这就是“索引下推”的用武之地(把c的过滤下推到存储引擎层)。

另一个容易翻车的是“回表”。如果查询需要的列在辅助索引里没有,就要回聚簇索引再查一次。覆盖索引(using index)就是为了避免回表。能结合EXPLAIN里的Extra字段说明“这个查询用到了覆盖索引,Extra显示Using index”,面试效果非常加分。

5.3 Redis持久化和缓存雪崩、穿透、击穿

Redis这块5年经验至少要被问两轮。一轮是“RDB和AOF怎么选”,一轮是“缓存雪崩怎么预防”。

RDB是fork子进程做快照,文件紧凑,适合备份恢复;但fork大实例时可能卡顿(写时复制导致页表复制开销大)。AOF以日志追加,实时性高,但日志膨胀,需要定期BGREWRITEAOF重写。我接触过的成熟项目多数是“AOF+RDB混合持久化”,也就是AOF重写时生成RDB格式的基快照,再叠加增量日志。

缓存雪崩的预防,除了设置过期时间加随机抖动(比如5分钟±30秒)这种常规操作,还可以走“两级缓存”:本地Cache先扛住,再让打到Redis的流量降级。缓存穿透的解法无非是空值缓存+布隆过滤器,但要注意布隆过滤器有误判率,不能百分之百相信“布隆说不在就一定不在”,恰恰相反,布隆说“可能在”才要回源。

5.4 Kafka为什么快:刷盘、顺序写、零拷贝

Kafka的高性能三板斧是5年面试的性格题:

  1. 顺序追加写:Kafka的Partition是append-only的日志,顺序写磁盘比随机写快几个数量级,这是“便宜又大碗”的优化;
  2. 页缓存:用操作系统的PageCache,读写都走缓存,消费端读数据不用经过JVM堆,这是“零拷贝”的基础;
  3. sendfile零拷贝:传统IO是“磁盘→内核缓冲→用户缓冲→socket缓冲→网卡”,sendfile把用户态这趟省了,数据直接从内核缓冲进网卡。

再追加一个“为什么消息不支持随机读”的问题:没有随机读,Kafka才能做顺序IO,这是磁盘物理特性决定的。为什么Kafka能当“消息队列”又能当“存储”用?因为它把日志当作唯一的truth,消费只是游标移动。

6. 设计模式与架构设计:从“会写”到“会设计”

5年经验面试,设计模式题目很少直接问“单例有几种写法”,而是给场景让你自己搭方案。这里我把最常见的几类真题列一下。

6.1 用策略模式干掉if-else,这题有标准解

场景题一般是这样: 你负责一个支付模块,支持微信、支付宝、银联、余额四种支付方式,怎么设计?

低级答案:if (type == "wechat") ... else if (type == "alipay") ...,每次新增渠道都改核心类,测试全回归。

标准答案:定义一个PayStrategy接口,每种支付方式一个实现类,注册到Spring容器里,用一个Map维护channelCode → strategy,新渠道来了加一个实现类,核心逻辑不用动。这就是开闭原则。再扩展一步,可以用@PostConstruct或者ApplicationContext.getBeansOfType自动注入,连Map的维护都省了。

有个加分项是“有些支付方式需要异步回调、有些要同步直连”,所以策略模式里每种的差异不只是算法,还有流程编排,这时候可以再叠加模板方法(定义流程骨架,下沉到子类)。

6.2 观察者模式在订单状态机里的应用

还有一道我特别爱问的场景题:订单从“创建→已支付→已发货→已签收→已完成”,每一步都要触发一堆动作(发短信、更新库存、通知财务、记录日志),你怎么设计?

如果你说“在Service里按顺序调用”,那每个状态变更都会依赖一堆具体类。观察者模式的思路是:订单状态变更时发布事件,一堆Listener监听这个事件,各自的动作互相不依赖。对于异步动作(短信、通知),监听器里可以再丢到MQ,实现解耦+削峰。

框架层面Spring的ApplicationEventPublisher就是干这个的,用@EventListener+@Async,比直接注入N个Service干净太多。另外,状态机本身用StateMachine(比如Spring Statemachine)可以管理“合法流转”,防止跳状态。

6.3 DDD不是让你背概念的,考察点是聚合边界

架构设计题,5年经验的人最好懂一点DDD。面试官不会让你背“实体、值对象、聚合根”的定义,而是给一个业务场景让画限界上下文。比如“电商系统怎么做领域划分”,你要能说出:订单上下文、库存上下文、支付上下文、营销上下文,它们之间通过领域事件通信,而不是共享一个大OrderService。

聚合根是另一个容易含糊的点。我面试时经常举一个例子:Order和OrderItem谁更该是聚合根?答案是Order,因为OrderItem脱离Order没有存在的意义,是订单内的部分,订单的操作(加商品、改数量、提交)都必须通过Order这个门面。很多人只说“两个实体、一对多”,那就和“贫血模型CRUD”没区别,看不出架构设计能力。

6.4 设计一个短URL系统,检验你的容量思维

这道题我不分年限地用来考“设计能力”。5年经验的答案应该长这样:

  • 发号器用数据库自增或Snowflake,把数值转成62进制;
  • 短码长度6位:62^6 ≈ 568亿,足够业务量了;
  • 写路径:发号器生成ID,写DB,再写Redis缓存;
  • 读路径:先查本地Cache,没有查Redis,没有再查DB;
  • 301还是302跳转?选302。因为301会被浏览器缓存,拿不到点击分析的数据;
  • 需要处理“短码被猜到的风险”:号段加签名校验,或者适当混淆。

这样的回答,一步步展示容量预估、读写分离、缓存分层、以及一个容易被忽略的业务决策(301 vs 302),面试官会觉得你真的设计过系统,而不是背了一套“高并发八股”。

6.5 秒杀系统的核心点:避免超卖和削峰

秒杀同样是5年面试的“常青树”。它的核心不是“高并发”,而是“不要超卖”和“把峰值打掉”。

  • 库存预扣:下单时先扣减Redis库存(Lua保证原子性),扣成功才写订单;失败了直接返回“已抢光”。
  • 削峰:请求先进MQ,消费者按库存量拉单处理,控制数据库压力。“异步削峰”比同步直写DB好得多。
  • 限流:每个用户单设备单接口限流,令牌桶或者滑动窗口。
  • 分层校验:前端按钮置灰,网关层拦截未登录,业务层再查黑名单,多一道少一道流量。
  • 兜底:数据库最终扣减要对得上账,如果Redis和DB数据不一致,对账任务发现后告警。

这套题的答案没有标准形式,但你要让面试官看到你脑子里有“流量从入口到DB的多层防线”。

7. 实操类场景题:笔试题与线上故障的真实考场

有的公司5年面试也会出机考或手写代码题,但和校招的算法题完全不同,基本都是“工程向”题目。所以我单独开一节讲“实操题”,这些题不刷到你写不出来,刷到过就能稳拿。

7.1 手写深拷贝:浅拷贝太容易被一眼看穿

搜索引擎热词里有“java对象深度拷贝”,这道题几乎年年出现。手写一个deepCopy方法,很多人的第一反应是序列化:

public static <T> T deepCopy(T obj) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(obj); oos.flush(); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); return (T) ois.readObject(); }

这段能跑,但有三个坑:

  1. 对象必须实现Serializable,否则直接抛异常;
  2. transient和static字段不会被拷贝,很多人忽略;
  3. 性能极差,序列化要对整个对象图做反射,大对象深拷贝会非常慢。

更可靠的方案是手动递归拷贝,只处理业务需要深拷贝的字段。比如“订单里有List ,我要考的是你能不能正确地把List和其中的对象也new出来”,而不是依赖序列化。

能提一句“实际项目中我用ORM的实体转VO时,更常用BeanUtils浅拷贝+手动设置需要深复制的字段,因为深拷贝大多数时候是反模式”,会显得你有经验。

7.2 判断字符串是否只含字母和数字,避开正则陷阱

搜索热词里有一道“java 判断字符串中是否不是字母和数字”,看似基础,其实能挖出性能坑。

常见答案是:Pattern.matches("^[a-zA-Z0-9]+$", str)。对于短字符串没问题,但如果这个方法要在一个高QPS的接口里被调用,正则每次都会编译一次(Pattern类内部有缓存,但肯定比直接判定慢),再遇到极端输入还可能退化成灾难性回溯。

稳妥写法是遍历字符:

public static boolean isAlphanumeric(String str) { if (str == null || str.isEmpty()) { return false; } for (int i = 0; i < str.length(); i++) { if (!Character.isLetterOrDigit(str.charAt(i))) { return false; } } return true; }

代码量差不多,但避免正则编译、避免回溯问题。这个场景就属于“基础但不简单”,能审出你有没有性能意识。字符串有中文时Character.isLetterOrDigit对汉字返回true,如果业务只要字母数字,需要改判定规则(ASCII区间判断)。

7.3 行级权限的实现,别只答“加个WHERE”

“行级权限java”是搜索热词里的一个典型业务题。常见需求是:销售只能看自己的客户,销售主管能看本组客户,财务能看全部客户。如果每段业务代码都手动加WHERE条件,漏一处就是越权事故。

成熟做法是在DAO层做统一拦截:定义一个数据权限注解,标注在Mapper接口方法上,用MyBatis拦截器在SQL执行前动态拼接权限过滤条件,根据当前登录用户的角色和归属机构,生成不同的WHERE条件。这就是规则引擎+拦截器思路。

但面试官会追问:“你自己拼SQL,怎么防止SQL注入?”。你要回答:拦截器里拼的是参数占位符,而不是直接拼接字符串;权限字段值是从认证上下文里取的,不是客户端传的。这两个细节一注意,行级权限方案才落地。

7.4 定时任务框架:Quartz、XXL-Job怎么选

“java定时任务框架”是热词里出现过的搜索需求。5年候选人不应该只会说“我用@Scheduled”,要说得出来它的局限:单机、无管理界面、失败没有重试。重任务要上分布式调度框架。

  • Quartz:提供了最基础的调度能力,持久化用JobStroe,能应对简单的单机定时任务。但到了分布式环境,“同一时刻只能有一个节点跑任务”是需要你手动用分布式锁去协调的。
  • XXL-Job:带控制台、执行器和调度器分离,支持失败重试、分片广播、动态调整Cron。我实际项目里用得最多,部署也简单。
  • ElasticJob(当当开源):内部直接依赖Zookeeper做分布式协调,适合已经有ZK的团队。

选型时还有一个点要提:任务分片。比如你有100万个用户要推送通知,不能一个任务跑到底,可以按节点数分片(每个节点处理总数/分片数),这样能充分利用多台机器,而不是一台打满其他空闲。

7.5 Controller防爬虫,说说方案分层

“java controller层 如何防护 防止爬虫”这条热词很特别,但特别贴近实战。防爬虫不是单一技术,而是一套“分层拦截”的组合拳:

  1. 网关/过滤器层:限流(令牌桶)、IP黑名单、陌生UA标识识别;
  2. 业务层:接口签名(AppKey+时间戳+签名)防止直接调接口,热数据设置访问频率;对明显爬虫的UA或高频IP,返回验证码挑战;
  3. 数据层:敏感字段加密返回,列表接口支持游标分页,防止爬虫到全量数据;
  4. 业务兜底:加入风控规则,比如同一账号一小时内请求超过N次,自动触发人机验证。

要提醒的是:防爬虫的核心是“延迟爬虫的边际收益”,不能让正常用户受干扰,所以在过滤器里做限流时要放行登录用户、放行静态资源,避免误伤。

7.6 外部排序和TopN,大数据文件的极限内存操作

如果你面试的是大数据偏多的业务,“java排序”“大数据文件排序”也可能被问到。比如一个文件里有几千万组数字,内存只有256MB,怎么排序?答案是外部排序:内存放不下的部分,分块排序写临时文件,最后归并。每块比如50MB,排序写完落盘,然后多路归并出完整顺序。

TopN问题更有实战价值:从几千万条记录里找排名前100的销售额,不用全排序,维护一个大小为100的小根堆,新元素大于堆顶就替换,复杂度从O(nlogn)降到O(nlog100)。注意这里细节是优先队列默认是小顶堆,取前K大要用小顶堆。

8. 我面了上百人之后,总结出的几点判断标准

这篇文章写了这么多题,最后说点面试之外的感悟。我每年面几十个人,发现能拿高薪offer的候选人和普通候选人的差距,往往不在“会不会背答案”,而在“有没有在项目里真实地解决过问题”。

首先,别把面试当考试,把它当成一次技术交流。你答不上的题,可以直接说“这个我确实没有深究过,但我的理解是……”,面试官最怕的是你明明不懂还在那边硬扯,反而浪费时间。我反过来会告诉你:“这个问题不会考”本身就是一种信号,说明你的知识边界被量化了。

其次,所有原理类的知识,一定要挂到具体业务上。synchronized锁升级、AQS队列、B+树结构,这些你在项目里可能一辈子都不需要改源码,但遇到线上性能问题,你会不会把思路引到“是不是多线程竞争导致重量级锁膨胀”“是不是索引没覆盖到导致回表”,才是这些原理真正的用武之地。

最后一点,5年经验是一个坎,过了这个坎,面试官真正想看的是你对一个系统的“整体把握能力”。你能画出自己负责模块的架构图,能说清楚每个环节的高峰流量、故障预案、数据一致性保障,这比任何一道面试题的标准答案都值钱。

如果你正准备跳槽,建议把这篇文章里的题按模块过一遍,每一道都试着用自己的项目经历去讲,讲不顺的标记下来查漏补缺。祝你能在面试里把节奏握在自己手里,遇到任何题都能沉着地说出“这个问题我们当时是这样处理的……”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询