我最近帮几个团队做模拟面试,发现一个挺有意思的现象:简历上写着“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飙高,你怎么排查
这是线上故障最典型的场景。候选人有没有实战经验,这道题一测就知道。标准链路应该是:
top -Hp看哪个线程CPU高,记下线程号;jstack导出线程栈,把线程号转成十六进制,在栈文件里搜;- 如果卡在GC线程,大概率是Full GC频繁,再
jstat -gcutil看GC频率和耗时; - 如果卡在业务线程,看是锁等待还是死循环,锁等待会停在
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()为例,核心路径是:
compareAndSetState(0, 1)尝试快速获取锁;- 失败就
addWaiter(Node.EXCLUSIVE)把当前线程封成Node,CAS挂到队列尾部; acquireQueued里循环:如果前驱是head,再次tryAcquire;否则shouldParkAfterFailedAcquire检查前驱状态,并LockSupport.park挂起线程;- 前驱节点释放锁之后,
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 幂等设计的三板斧
分布式题里“幂等设计”是必问的,而且特别务实。记住三个工具:
- 唯一键约束:数据库唯一索引兜底,比如支付订单号;
- 状态机:订单状态流转只能“已支付→已发货→已签收”,重复请求打到已签收就直接拒绝;
- 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年的人也说不出全流程。标准答案可以这样组织:
- 实例化(构造器或工厂方法);
- 属性填充(
populateBean,按名字或类型注入); Aware接口回调(BeanNameAware、BeanFactoryAware、ApplicationContextAware);BeanPostProcessor.beforeInitialization;@PostConstruct;InitializingBean.afterPropertiesSet;- 自定义
init-method; BeanPostProcessor.afterInitialization;- 如果处于使用中,正常被调用;
@PreDestroy;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年面试的性格题:
- 顺序追加写:Kafka的Partition是append-only的日志,顺序写磁盘比随机写快几个数量级,这是“便宜又大碗”的优化;
- 页缓存:用操作系统的PageCache,读写都走缓存,消费端读数据不用经过JVM堆,这是“零拷贝”的基础;
- 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(); }这段能跑,但有三个坑:
- 对象必须实现
Serializable,否则直接抛异常; transient和static字段不会被拷贝,很多人忽略;- 性能极差,序列化要对整个对象图做反射,大对象深拷贝会非常慢。
更可靠的方案是手动递归拷贝,只处理业务需要深拷贝的字段。比如“订单里有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层 如何防护 防止爬虫”这条热词很特别,但特别贴近实战。防爬虫不是单一技术,而是一套“分层拦截”的组合拳:
- 网关/过滤器层:限流(令牌桶)、IP黑名单、陌生UA标识识别;
- 业务层:接口签名(AppKey+时间戳+签名)防止直接调接口,热数据设置访问频率;对明显爬虫的UA或高频IP,返回验证码挑战;
- 数据层:敏感字段加密返回,列表接口支持游标分页,防止爬虫到全量数据;
- 业务兜底:加入风控规则,比如同一账号一小时内请求超过N次,自动触发人机验证。
要提醒的是:防爬虫的核心是“延迟爬虫的边际收益”,不能让正常用户受干扰,所以在过滤器里做限流时要放行登录用户、放行静态资源,避免误伤。
7.6 外部排序和TopN,大数据文件的极限内存操作
如果你面试的是大数据偏多的业务,“java排序”“大数据文件排序”也可能被问到。比如一个文件里有几千万组数字,内存只有256MB,怎么排序?答案是外部排序:内存放不下的部分,分块排序写临时文件,最后归并。每块比如50MB,排序写完落盘,然后多路归并出完整顺序。
TopN问题更有实战价值:从几千万条记录里找排名前100的销售额,不用全排序,维护一个大小为100的小根堆,新元素大于堆顶就替换,复杂度从O(nlogn)降到O(nlog100)。注意这里细节是优先队列默认是小顶堆,取前K大要用小顶堆。
8. 我面了上百人之后,总结出的几点判断标准
这篇文章写了这么多题,最后说点面试之外的感悟。我每年面几十个人,发现能拿高薪offer的候选人和普通候选人的差距,往往不在“会不会背答案”,而在“有没有在项目里真实地解决过问题”。
首先,别把面试当考试,把它当成一次技术交流。你答不上的题,可以直接说“这个我确实没有深究过,但我的理解是……”,面试官最怕的是你明明不懂还在那边硬扯,反而浪费时间。我反过来会告诉你:“这个问题不会考”本身就是一种信号,说明你的知识边界被量化了。
其次,所有原理类的知识,一定要挂到具体业务上。synchronized锁升级、AQS队列、B+树结构,这些你在项目里可能一辈子都不需要改源码,但遇到线上性能问题,你会不会把思路引到“是不是多线程竞争导致重量级锁膨胀”“是不是索引没覆盖到导致回表”,才是这些原理真正的用武之地。
最后一点,5年经验是一个坎,过了这个坎,面试官真正想看的是你对一个系统的“整体把握能力”。你能画出自己负责模块的架构图,能说清楚每个环节的高峰流量、故障预案、数据一致性保障,这比任何一道面试题的标准答案都值钱。
如果你正准备跳槽,建议把这篇文章里的题按模块过一遍,每一道都试着用自己的项目经历去讲,讲不顺的标记下来查漏补缺。祝你能在面试里把节奏握在自己手里,遇到任何题都能沉着地说出“这个问题我们当时是这样处理的……”。