一、 JDK21 虚拟线程(Virtual Thread)深度剖析
Q1:虚拟线程和平台线程(传统线程)有什么区别?底层是如何实现的?
思路: 从调度模型、内存占用、创建开销三个维度回答。
- 调度模型不同:平台线程是操作系统原生线程,由OS调度;虚拟线程是JVM管理的轻量级线程(Project Loom项目),由JVM在用户态(Carrier线程,即平台线程)上进行调度(M:N模型)。
- 内存与开销:平台线程默认栈空间较大(1MB左右),创建和销毁开销大;虚拟线程栈是动态的(类似协程),初始很小(几MB),创建成本极低,可以轻松创建百万级实例。
- 阻塞处理:当虚拟线程遇到IO阻塞(如网络请求、文件读写)时,JVM会将其从载体线程上"卸载",让载体线程去执行其他虚拟线程,IO恢复后再"挂载"回去,从而实现高并发。
Q2:用虚拟线程重构了异步线程池,具体怎么做?和原来的 CompletableFuture/Reactive 方案比有什么优劣?
思路: 强调"代码简化"和"性能提升",承认其局限性。
做法:以前处理一个复杂业务流程可能需要拆分成多个 CompletableFuture 链式调用,或者使用WebFlux的 Mono/Flux ,代码回调地狱严重。重构时,将阻塞IO操作(如调用下游服务、访问数据库)直接放在虚拟线程中执行,保持了代码的同步写法( imperative style )。
优势:代码可读性极高,像写同步代码一样写高并发程序,维护成本低。且虚拟线程的上下文切换由JVM负责,比手动管理线程池和回调要高效。
劣势:虽然吞吐量大,但对于CPU密集型任务,由于受限于载体线程(Platform Thread)的数量,虚拟线程并不能无限提升性能,甚至可能因为频繁的挂起/恢复带来额外开销。
Q3:synchronized 会不会钉住(Pinning)虚拟线程?如果一定要用 synchronized 怎么办?
思路: 解释"钉住"的原因,给出解决方案( ReentrantLock 或 synchronized 优化)。
会钉住:当虚拟线程在执行 synchronized 代码块且持有了监视器锁(Monitor)时,如果这个锁是偏向锁或轻量级锁,或者虚拟机实现的原因,虚拟线程可能会被"钉住"在当前载体线程(Carrier Thread)上,无法被挂起,直到它释放锁。这会阻塞载体线程,影响整体吞吐量。
解决方案:
替换锁:尽量使用 java.util.concurrent.locks.ReentrantLock ,它支持可中断、公平锁等高级特性,且不会像 synchronized 那样容易导致钉住。
减小锁粒度:尽量不要在大的代码块上使用 synchronized ,缩小临界区。
升级JDK/调整参数:在较新的JDK版本中,JVM对 synchronized 在虚拟线程下的表现做了优化,或者在启动参数中调整相关配置(但这通常是最后手段)。
二、 数据库与 SQL 优化
Q4:如果一个核心接口需要关联8张表,怎么优化的?
思路: 从"业务抽象 -> 技术拆分 -> 异步化"三个层面回答,体现一面的"应变能力"。
业务抽象与需求对齐:和PM/业务方沟通:是否真的需要实时返回8张表的全部聚合数据?能否分层展示,或者异步生成报表?
垂直/水平拆分:
垂直:如果8张表代表不同的业务域,考虑拆分成多个接口,前端按需加载。
水平:对于多表Join,如果数据量巨大,会尝试将Join操作下沉到服务层(Application Layer),而不是在DB层做。即先查主表,再根据主表ID批量查询其他表(N+1查询变IN查询),利用Redis缓存热点数据,减少DB压力。
宽表与物化视图:对于报表类、统计分析类的多表Join,考虑在离线计算(如Spark/Hive)或准实时数仓中构建宽表,或者使用数据库的物化视图来预计算,避免在线业务库做复杂的Join。
异步化:如果查询非常重,将其改为异步任务,生成结果后通知用户下载。
Q5:千万级订单列表查询,除了索引和分页,还可以做哪些优化?
思路: 覆盖"查询条件优化"、"深分页优化"、"缓存策略"、"数据归档"。
查询条件优化:确保 WHERE 条件中的字段都有索引,避免全表扫描。对于模糊查询(如订单号、收货人),尽量使用前缀匹配或引入搜索引擎(ES)。
深分页优化:传统的 LIMIT offset, size 在数据量大时很慢,因为数据库要扫描前 offset 条数据。采用了基于游标(Cursor)的分页,或者延迟关联(先查ID,再根据ID关联详情),性能提升明显。
数据归档:对于历史订单,定期进行归档(冷热分离),将冷数据迁移到历史表或归档库(如HBase),减少在线表的数据量。
缓存穿透/击穿:对热点查询条件增加本地缓存(Caffeine)或分布式缓存(Redis),并对空结果进行缓存,防止恶意请求打满数据库。
三、 消息队列与分布式事务(RocketMQ)
Q6:RocketMQ事务消息。它的基本实现原理是什么?半消息(Half Message)是什么?
思路: 解释"两阶段提交"思想在MQ中的体现。
原理:RocketMQ事务消息基于两阶段提交(2PC)思想。
第一阶段(发送半消息):发送一条"半消息"(Half Message)到Broker,Broker会暂存这条消息,不会投递给消费者。同时,生产者会执行本地事务逻辑。
第二阶段(确认/回滚):生产者根据本地事务执行结果,向Broker发送 commit 或 rollback 指令。
如果 commit ,Broker会将半消息变为"可消费消息",投递给消费者。
如果 rollback ,Broker会丢弃这条半消息。
回查机制(补救):如果生产者在规定时间内没有发送 commit/rollback (如宕机),Broker会定时回调生产者的 checkLocalTransaction 接口,询问本地事务的执行状态,再决定最终操作。
半消息:就是已经被发送到Broker,但处于"不可见"或"暂存"状态,等待生产者最终确认的消息。
Q7:顺序消息(顺序消息)在RocketMQ中是怎么保证的?如果要保证全局顺序,需要注意什么?
思路: 区分"分区有序"和"全局有序"。
保证机制:RocketMQ的顺序消息是通过MessageQueueSelector实现的。生产者发送消息时,通过一个特定的选择器(如根据订单ID哈希),将同一个业务逻辑的消息发送到同一个MessageQueue中。因为同一个MessageQueue内的消息是顺序存储和消费的,所以能保证局部(分区)有序。
全局顺序:要保证全局顺序,必须让所有消息都发到同一个MessageQueue中。但这会失去分布式并行处理的优势,吞吐量会极低。因此,实际生产中很少用全局顺序,都是用分区顺序,即保证同一笔订单的操作顺序即可。
消费端注意:消费端必须是单线程消费同一个MessageQueue,或者使用 MessageListenerOrderly (顺序监听器),否则多线程消费还是会打乱顺序。
四、 多线程与线程池
Q8:"自定义线程池、线程隔离"。线程池的核心参数有哪些?拒绝策略有哪些?怎么选?
思路: 默写参数,结合实际场景解释选择。
核心参数: corePoolSize (核心线程数)、 maximumPoolSize (最大线程数)、 keepAliveTime (非核心线程存活时间)、 workQueue (工作队列)、 threadFactory (线程工厂)、 handler (拒绝策略)。
拒绝策略:
AbortPolicy (默认):直接抛出 RejectedExecutionException ,阻止系统正常运行。
CallerRunsPolicy :由调用者线程(提交任务的线程)执行任务,减缓新任务提交速度。
DiscardPolicy :直接丢弃任务,不抛异常。
DiscardOldestPolicy :丢弃队列中最老的任务,然后尝试重新提交当前任务。
选择与应用:为了防止大任务抢占核心业务资源,使用了线程隔离。单独配置了一个线程池(核心数小,队列大),拒绝策略选择了 CallerRunsPolicy ,这样当导出任务过多时,会由提交任务的Web线程来执行,从而起到一种"负反馈"作用,让前端请求变慢,但不会拖垮整个系统。
五、 系统设计 & 架构思维
Q9:自研了"通用异步报表导出引擎"的架构的设计思路,为什么要用策略模式+泛型?
思路: 体现"解耦"、"复用"、"扩展性"。
痛点:各个业务线(订单、对账、承运商)都有导出需求,如果每个都写一套导出逻辑,会有大量重复代码,且难以维护。
设计思路:
策略模式 + 泛型:定义了一个通用的导出接口 ExportStrategy<T> ,其中 T 是泛型,代表不同的数据类型(订单DTO、对账DTO等)。每个业务方只需要实现这个接口,编写自己的"数据查询"和"Excel渲染"逻辑即可。
模板方法:引擎内部有一个抽象的模板方法,负责"接收请求 -> 生成任务ID -> 放入线程池 -> 返回任务ID给前端"的通用流程。
线程隔离与限流:引擎层维护了多个独立的线程池,不同业务的导出任务互不干扰。同时使用Redis对任务并发数进行限流,防止DB和IO被打满。
异步回调与下载:任务在后台线程执行,生成文件后上传到OSS,并通过MQ或WebSocket通知前端,前端凭链接下载。
优势:业务方无需关心异步、线程池、限流等复杂逻辑,只需关注"怎么查数据"和"怎么渲染",大大提高了开发效率和系统稳定性。
六、 软实力 & 应变能力
Q10:如何推动"业务架构"层面的优化的吗?
思路: 讲一个真实(或合理虚构)的故事,体现沟通能力和技术影响力。
场景:在做物流轨迹大屏展示时,最初需求是实时聚合查询8张表(订单、运单、节点、车辆、司机、仓库、异常、签收),数据量巨大,SQL跑不动。
行动:不死磕SQL优化,而是拉上产品经理和数据产品,分析他们真正关心的指标是什么。我们发现,其实很多数据是"次要"的,前端展示时会被折叠或聚合。
结果:和前端协商,将"一次性全量加载"改为"分层加载 + 按需展开"。第一层只查3张核心表(订单、运单、最新节点),第二层点击"查看详情"时再通过接口单独查其他表。同时,对于历史数据,我们引导产品使用"离线报表"代替实时查询。
价值:最终,核心接口的响应时间从5秒降到了200毫秒,DB压力大幅降低,同时也简化了前端交互,获得了业务的认可。这说明技术优化不仅仅是写代码,更是通过技术手段引导业务做出更合理的设计。
回答技术问题一定要有"场景 -> 做法 -> 原理/思考"的结构。一定要能讲出"为什么这么做"、"有没有其他方案"、"权衡点在哪里"。