Java实习模拟面试|字节跳动TTS后端校招二面面经:WaitGroup性能优化、分布式锁实现、线程安全LRU与Optional实战深度解析
关键词:字节跳动TTS后端|WaitGroup原理|分布式锁|SETNX|线程安全LRU|Java Optional|高并发优化|CSDN面经
在字节跳动TTS(Text-to-Speech)后端校招的第二轮技术面试中,面试官聚焦于高并发系统优化、分布式协调、内存数据结构设计三大核心能力。作为支撑亿级语音合成请求的基础设施团队,TTS后端对性能、一致性、资源利用率的要求极为严苛。
本文以高度还原的真实对话形式,完整复现这场60分钟的技术拷打。从百度实习项目深挖,到WaitGroup性能收益量化,再到手撕线程安全LRU缓存,全程贯穿“为什么这么做?有没有更好方案?边界 case 如何处理?” 的工程师思维训练。
无论你目标是字节、腾讯还是其他大模型/AI基础设施团队,这篇面经都将为你提供清晰的技术纵深路径!
1. 介绍在百度干的活
面试官提问:你在百度实习期间具体做了什么?用到了哪些关键技术?
我:
我在百度智能云参与一个多模态内容生成平台的后端开发,核心模块是批量任务调度引擎。用户提交一批文本(比如1000条),系统需要并行调用TTS、图像生成、视频合成等服务,最后聚合结果返回。
技术栈上:
- 用 Go 写了任务分发器(因高并发需求);
- Java 负责结果聚合、状态管理、OSS存储;
- 关键优化点:使用
sync.WaitGroup控制并发子任务,避免主线程过早返回。
追问:为什么不用 Java 全栈?Go 和 Java 如何通信?
我:
初期全 Java,但发现轻量级协程 + 高并发 I/O场景下,Go 的 goroutine 比 Java 线程更省资源(MB vs KB)。我们通过gRPC实现 Go 调度器与 Java 聚合服务通信,Protobuf 定义统一接口,QPS 提升3倍,P99 延迟从800ms降到200ms。
2. WaitGroup 使用效果与底层原理
面试官提问:你提到用
WaitGroup节省了时间,具体节省了多少?为什么要省这些时间?它的底层是怎么实现的?
我:
(1)节省了多少时间?
- 优化前:串行处理1000个任务,平均耗时12秒(每个任务12ms);
- 优化后:并发100 goroutine + WaitGroup,耗时150ms,提速80倍。
(2)为什么要省这些时间?
- 用户体验:用户等待超过1秒就会感知卡顿;
- 资源成本:长连接占用网关/DB连接池,影响系统吞吐;
- SLA要求:内部P0接口要求 P99 < 500ms。
(3)WaitGroup 底层原理
WaitGroup本质是一个带计数器的信号量,基于atomic + semaphore实现:
Add(delta):原子增加 counter;Done():原子减1,若 counter==0,则释放所有等待的 goroutine;Wait():若 counter>0,调用runtime_Semacquire阻塞当前 goroutine。
关键点:它不是锁,而是同步原语,用于协调多个 goroutine 的生命周期。
追问:如果
Add()在Wait()之后调用会怎样?
我:
会 panic!因为WaitGroup内部 counter 初始为0,Wait()发现 counter==0 会直接返回。若后续再Add(1),再Done(),counter 变成 -1,触发panic("sync: negative WaitGroup counter")。
最佳实践:先 Add,再启动 goroutine,最后 Wait。
3–4. 分布式锁与 SETNX 原理
面试官提问:你们系统里有没有用到分布式锁?怎么实现的?
我:
有!在结果文件合并阶段,多个 worker 可能同时完成任务,需保证只有一个能执行最终合并操作。我们用Redis + SETNX实现:
// 伪代码StringlockKey="merge_lock:"+taskId;StringrequestId=UUID.randomUUID().toString();// 获取锁Booleanlocked=redis.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_EXPIRE,30,SECONDS);if(locked){try{// 执行合并逻辑}finally{// 释放锁(Lua脚本保证原子性)redis.eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",Collections.singletonList(lockKey),Collections.singletonList(requestId));}}追问:为什么用 Lua 脚本释放锁?直接 DEL 不行吗?
我:
不行!存在误删风险:
- 线程A获取锁,超时未释放;
- 线程B获取到同一把锁;
- 此时线程A执行
DEL,会把线程B的锁删掉!
用 Lua 脚本可原子判断+删除:只有 value(requestId)匹配才删除。
SETNX 原理
SETNX key value=SET if Not eXists- Redis 单线程执行,天然原子;
- 返回 1 表示加锁成功,0 表示已被占用。
缺陷:不支持可重入、锁过期时间难设置(太短易失效,太长易死锁)。
进阶方案:Redlock(多实例)、ZooKeeper(临时顺序节点)。
5. 手撕算法:LRU 缓存
面试官提问:手写一个 LRU(Least Recently Used)缓存,要求 get/put 时间复杂度 O(1)。
我:
使用HashMap + 双向链表组合:
- HashMap 存
<key, Node>,O(1) 查找; - 双向链表维护访问顺序,头为最新,尾为最旧。
classLRUCache{classNode{intkey,val;Nodeprev,next;Node(intk,intv){key=k;val=v;}}privateMap<Integer,Node>cache=newHashMap<>();privateNodehead,tail;privateintcapacity;publicLRUCache(intcapacity){this.capacity=capacity;head=newNode(0,0);tail=newNode(0,0);head.next=tail;tail.prev=head;}privatevoidaddToHead(Nodenode){node.next=head.next;node.prev=head;head.next.prev=node;head.next=node;}privatevoidremoveNode(Nodenode){node.prev.next=node.next;node.next.prev=node.prev;}privatevoidmoveToHead(Nodenode){removeNode(node);addToHead(node);}publicintget(intkey){Nodenode=cache.get(key);if(node==null)return-1;moveToHead(node);returnnode.val;}publicvoidput(intkey,intvalue){Nodenode=cache.get(key);if(node!=null){node.val=value;moveToHead(node);}else{NodenewNode=newNode(key,value);cache.put(key,newNode);addToHead(newNode);if(cache.size()>capacity){Nodelast=tail.prev;removeNode(last);cache.remove(last.key);// ⚠️ 必须移除map中的key!}}}}面试官追问1:如果要保证线程安全,在哪里加锁?
我:
LRU 的get和put都涉及共享状态修改(链表结构调整 + map更新),必须加锁。有两种方案:
方案一:粗粒度锁
- 整个类加
synchronized或ReentrantLock; - 简单但并发度低,所有操作串行。
方案二:读写锁(推荐)
get用readLock(),允许多读;put用writeLock(),独占写;- 提升并发性能。
privatefinalReadWriteLocklock=newReentrantReadWriteLock();publicintget(intkey){lock.readLock().lock();try{/* ... */}finally{lock.readLock().unlock();}}publicvoidput(intkey,intvalue){lock.writeLock().lock();try{/* ... */}finally{lock.writeLock().unlock();}}注意:即使读操作,也可能触发
moveToHead(写链表),所以不能无锁!
面试官追问2:Java 中有没有用过
Optional?它解决了什么问题?
我:
当然!Optional<T>是 Java 8 引入的空安全容器,主要解决:
- 避免
NullPointerException; - 显式表达“可能为空”的语义;
- 链式处理空值。
典型用法:
// 传统写法if(user!=null&&user.getAddress()!=null){Stringcity=user.getAddress().getCity();}// Optional 写法Stringcity=Optional.ofNullable(user).map(User::getAddress).map(Address::getCity).orElse("Unknown");在 LRU 中的应用:
publicOptional<Integer>getOptional(intkey){intval=get(key);returnval==-1?Optional.empty():Optional.of(val);}这样调用方无需猜测-1是否代表“不存在”,API 更清晰。
总结:字节TTS后端二面考察重点
| 能力维度 | 考察点 | 应对建议 |
|---|---|---|
| 工程优化 | WaitGroup 性能收益量化 | 准备具体数据(QPS/P99/成本) |
| 分布式协调 | 分布式锁实现细节 | 掌握 SETNX + Lua 释放 + Redlock |
| 数据结构 | LRU 设计与线程安全 | 手写 HashMap+双向链表,理解锁粒度 |
| 现代Java | Optional 使用场景 | 用函数式风格替代 null 判断 |
字节TTS团队偏好:
- 能量化优化效果的工程师;
- 对底层原理(如 Redis 原子性、JVM 锁)有好奇心;
- 代码追求健壮性(线程安全、空安全、边界处理)。
觉得这篇面经干货满满?欢迎点赞 + 收藏 + 关注!