news 2026/9/22 22:52:59

5个避坑点,手把手教你搞定哔哩哔哩招聘手写题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
5个避坑点,手把手教你搞定哔哩哔哩招聘手写题

5个避坑点,手把手教你搞定哔哩哔哩招聘手写题

配置环境就卡半天,是不是你的常态?

别急着骂系统,大概率是你没搞懂底层逻辑。

很多B站后端开发面试题,表面看是算法,实则考的是最佳实践中的工程化思维。

我在掘金技术社区看到不少大牛复盘,发现80%的人挂在了“环境适配”和“边界条件”上。

今天这篇,不灌鸡汤,直接拆解哔哩哔哩招聘中最高频的手写实现考点。

我们聚焦三个核心:并发控制状态机转换数据结构优化

这也是目前大厂面试中最能拉开差距的部分。

一、 为什么你写的代码跑不通?

很多人以为手写题考的是“背题”,错了。

它考的是你如何在受限环境下,写出可运行、可维护、无Bug的代码。

以B站常见的“视频播放进度上报”场景为例。

面试官不会让你直接调API,而是给你一个空的类,让你实现核心逻辑。

这时候,90%的人第一反应是:用个 HashMap 存状态。

这就踩坑了。

为什么?因为并发安全被忽略了。

在真实的高并发场景下,多个线程同时修改同一个视频的用户进度,HashMap 会直接报 ConcurrentModificationException

更严重的是,数据不一致。

A用户看到进度100%,B用户看到50%,这在业务上是灾难。

原理简述:

我们需要的是一个线程安全的状态容器,且状态转换必须符合业务逻辑。

这就引入了**状态机(State Machine)**的概念。

状态机不是高深理论,它是处理“有限状态、明确转换规则”问题的最佳实践

二、 状态机:视频进度的底层逻辑

把视频播放想象成一个地铁系统。

每个视频进度就是一个“站点”。

用户只能从“未开始”到“播放中”,再到“暂停”,或者“结束”。

你不能直接从“未开始”跳到“结束”,除非你是快进,但快进也有速度限制。

这就是状态约束

在代码层面,我们需要定义:

  1. 状态(State):当前视频处于什么阶段。
  2. 事件(Event):用户做了什么操作(如点击播放、暂停、拖动进度条)。
  3. 转换(Transition):在什么状态下,收到什么事件,会变成什么新状态。

这种结构,天然避免了非法状态的出现。

比如,你不能在“暂停”状态下,再次收到“暂停”事件后,状态变成“播放中”。

逻辑必须闭环。

三、 源码拆解:手写一个线程安全的状态机

下面这段代码,是我在模拟B站面试环境时,反复打磨过的版本。

它解决了并发问题,也体现了最佳实践中的防御性编程思想。

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;public class VideoProgressStateMachine {// 定义状态枚举public enum State {INIT,      // 初始状态PLAYING,   // 播放中PAUSED,    // 暂停FINISHED   // 结束}// 定义事件枚举public enum Event {START,     // 开始播放PAUSE,     // 暂停RESUME,    // 继续播放SEEK,      // 拖动进度条FINISH     // 播放结束}// 状态转换表:State + Event -> NewState// 使用 ConcurrentHashMap 保证初始化时的线程安全private static final ConcurrentHashMap<State, ConcurrentHashMap<Event, State>> TRANSITIONS = new ConcurrentHashMap<>();static {// 初始化状态转换规则// 注意:这里只定义合法转换,非法转换默认抛异常或忽略TRANSITIONS.put(State.INIT, new ConcurrentHashMap<>());TRANSITIONS.get(State.INIT).put(Event.START, State.PLAYING);TRANSITIONS.put(State.PLAYING, new ConcurrentHashMap<>());TRANSITIONS.get(State.PLAYING).put(Event.PAUSE, State.PAUSED);TRANSITIONS.get(State.PLAYING).put(Event.FINISH, State.FINISHED);TRANSITIONS.get(State.PLAYING).put(Event.SEEK, State.PLAYING); // 拖动后仍在播放TRANSITIONS.put(State.PAUSED, new ConcurrentHashMap<>());TRANSITIONS.get(State.PAUSED).put(Event.RESUME, State.PLAYING);TRANSITIONS.get(State.PAUSED).put(Event.SEEK, State.PAUSED);  // 拖动后仍暂停TRANSITIONS.get(State.PAUSED).put(Event.FINISH, State.FINISHED);TRANSITIONS.put(State.FINISHED, new ConcurrentHashMap<>());// FINISHED 是终态,通常不允许再转换,除非重置}// 当前状态,使用 AtomicReference 保证原子性更新private final AtomicReference<State> currentState = new AtomicReference<>(State.INIT);// 回调函数,状态变化后执行private BiFunction<State, Event, Void> onTransitionCallback;public void setOnTransitionCallback(BiFunction<State, Event, Void> callback) {this.onTransitionCallback = callback;}/*** 核心方法:发送事件,触发状态转换* @param event 事件* @return 是否转换成功*/public boolean sendEvent(Event event) {State current = currentState.get();State nextState = getValidTransition(current, event);if (nextState == null) {// 非法状态转换,记录日志,返回false// 在实际项目中,这里应该接入监控系统System.err.println("Invalid transition: " + current + " -> " + event);return false;}// 原子性更新状态,只有当前状态确实是current时,才更新为nextState// 这防止了两个线程同时读取到INIT,都试图转换为PLAYINGboolean updated = currentState.compareAndSet(current, nextState);if (updated) {// 状态更新成功,触发回调if (onTransitionCallback != null) {onTransitionCallback.apply(nextState, event);}return true;}// 更新失败,说明状态已被其他线程修改,需要重试// 在实际高并发场景下,这里可以加一个重试机制return sendEvent(event);}private State getValidTransition(State current, Event event) {ConcurrentHashMap<Event, State> eventsMap = TRANSITIONS.get(current);if (eventsMap == null) return null;return eventsMap.get(event);}public State getCurrentState() {return currentState.get();}// 测试代码public static void main(String[] args) {VideoProgressStateMachine sm = new VideoProgressStateMachine();sm.setOnTransitionCallback((newState, event) -> {System.out.println("State changed to: " + newState + " via event: " + event);return null;});System.out.println("Current State: " + sm.getCurrentState()); // INITsm.sendEvent(Event.START); // 变为 PLAYINGsm.sendEvent(Event.PAUSE); // 变为 PAUSEDsm.sendEvent(Event.RESUME);// 变为 PLAYINGsm.sendEvent(Event.FINISH);// 变为 FINISHED// 尝试非法转换sm.sendEvent(Event.START); // 应该报错,因为FINISHED是终态}
}

逐行讲解关键点

  1. AtomicReference vs synchronized: 很多新手喜欢用 synchronized 锁住整个 sendEvent 方法。 这没错,但性能差。 在高并发下,锁竞争严重。 AtomicReferencecompareAndSet 是基于 CAS(Compare-And-Swap)操作,无锁,性能更高。 这是最佳实践中的典型优化。

  2. 状态转换表(Transition Table): 我没有用一堆 if-else 判断状态。 而是用一个二维映射表。 这样做的好处是:逻辑与数据分离。 如果业务需求变了,比如“暂停”状态下允许直接“结束”,你只需要改配置表,不用改核心逻辑代码。 这就是开闭原则的体现。

  3. 重试机制(Retry): 注意 sendEvent 里的递归调用。 如果 CAS 失败,说明状态变了,我们需要基于最新的状态再次尝试转换。 这在并发编程中非常关键。 如果不去重试,直接返回 false,可能会导致用户操作丢失。

四、 流程描述:从用户点击到状态更新

让我们用文字描述一下这段代码在真实系统中的流转过程。

  1. 用户操作:用户在B站APP点击“暂停”按钮。
  2. 前端请求:前端发送 HTTP 请求,携带视频ID和用户ID。
  3. 服务端接收:B站后端网关接收请求,路由到具体的视频服务实例。
  4. 实例获取:服务实例从缓存或内存中获取该用户对应的 VideoProgressStateMachine 实例。
    • 注:每个用户每个视频对应一个独立的状态机实例,避免互斥。
  5. 状态检查:调用 sendEvent(Event.PAUSE)
  6. CAS 竞争
    • 线程A读取当前状态为 PLAYING
    • 线程A尝试将状态从 PLAYING 更新为 PAUSED
    • 如果成功,继续下一步。
    • 如果失败(说明其他线程刚改了状态),线程A重新读取状态,再次尝试。
  7. 回调执行:状态更新成功后,触发回调函数。
    • 回调函数可能执行:更新数据库进度、推送WebSocket消息给前端、记录埋点数据。
  8. 响应返回:服务端返回 200 OK,前端UI更新为“暂停”图标。

这个流程中,状态机保证了核心逻辑的一致性,CAS 保证了并发的安全性,回调 实现了业务逻辑的解耦。

五、 实战验证与避坑指南

在掘金技术社区的很多讨论中,大家常问:“如果状态转换太频繁,CAS 一直失败怎么办?”

这就是我们要讲的进阶技巧

1. 自适应自旋

如果 CAS 失败,不要立即重试。 可以加入一个短暂的 Thread.yield() 或者 LockSupport.park()。 让出 CPU 时间片,等待其他线程完成操作。 避免“忙等待”导致 CPU 空转。

2. 批量操作优化

如果用户快速拖动进度条,会产生大量 SEEK 事件。 如果每个事件都触发一次数据库更新,数据库会崩。

最佳实践: 在回调函数中,不要直接写库。 而是将事件放入一个内存队列本地缓冲区。 通过定时任务(如每 500ms)或队列满时,批量更新数据库。

代码示例:

// 在回调函数中
private BiFunction<State, Event, Void> onTransitionCallback = (newState, event) -> {if (event == Event.SEEK || event == Event.PAUSE) {// 不直接写库,加入缓冲区progressBuffer.add(new ProgressRecord(newState, event, System.currentTimeMillis()));return null;}// 其他事件直接处理handleImmediateEvent(newState, event);return null;
};

3. 内存泄漏风险

ConcurrentHashMap 如果一直往里放数据,不清理,会 OOM。

解决方案

  • 使用 WeakHashMapSoftHashMap 缓存状态机实例。
  • 当用户长时间不活跃,GC 回收时,自动清理状态机。
  • 或者设置 TTL(Time-To-Live),定时扫描清理过期实例。

4. 为什么不用 ReentrantLock

有些面试官会问:“既然 CAS 会失败,为什么不用 ReentrantLock 保证互斥?”

回答要点:

  • 粒度不同ReentrantLock 是阻塞式,线程会挂起,上下文切换开销大。
  • 场景不同:状态转换操作极短(纳秒级),CAS 的失败率虽然存在,但重试成本远低于锁的获取成本。
  • 公平性ReentrantLock 可以保证公平性,但状态转换不需要严格的公平性,只要最终一致即可。

六、 总结与互动

这篇内容,我们拆解了哔哩哔哩招聘中典型的状态机手写题。

核心在于:

  1. 理解业务本质:视频进度是有状态约束的,不是随意变动的。
  2. 选择合适工具:用 AtomicReference 处理并发,用 Map 管理转换规则。
  3. 考虑工程细节:重试机制、批量更新、内存清理,这些才是区分初级和高级开发的最佳实践

不要只盯着算法题的“解法”,要盯着业务场景的“痛点”。

配置环境卡半天,往往是因为你没看懂代码背后的设计意图。

希望这篇文章,能帮你理清思路,下次遇到类似的手写题,能从容应对。

你公司项目里,是怎么处理这种高频状态更新的?是用锁,还是用无锁结构?欢迎在评论区聊聊你的实战经验。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/22 22:52:56

一文搞懂微星主板怎么样,3步定位性能瓶颈

一文搞懂微星主板怎么样,3步定位性能瓶颈 别被那些花哨的RGB灯效迷了眼。很多老鸟踩坑后发现, 微星主板怎么样 这个问题,答案往往不在包装盒上,而在你项目跑满负载时的温度墙和内存延迟里。你是不是也遇到过这种情况: 学会语法却不知怎么搭项目…

作者头像 李华
网站建设 2026/9/22 22:52:01

5道高频面试题拆解陋室空堂,避开90%新人踩坑的选型误区

5道高频面试题拆解陋室空堂,避开90%新人踩坑的选型误区 面试被问原理答不上来,那种瞬间大脑一片空白的感觉,谁懂?特别是当面试官抛出“陋室空堂”这种看似冷门实则考察底层逻辑的 高频面试题…

作者头像 李华
网站建设 2026/9/22 22:52:00

3行代码看懂their本质:告别官方文档迷雾的实战指南

3行代码看懂their本质:告别官方文档迷雾的实战指南 官方文档那几万字,谁读得完?别跟我扯什么“耐心研读”,真在一线摸爬滚打的人,要的是立刻能跑通、能落地、能解决线上Bug的东西。 我见过太多人,在GitHub…

作者头像 李华
网站建设 2026/9/22 22:51:54

Star怎么读?源码解析揭秘后端新手3大避坑点

Star怎么读?源码解析揭秘后端新手3大避坑点 刚接手新项目,从 GitHub 开源仓库 抄了一段 Star 处理逻辑,结果一跑就崩?别慌,这锅不全是代码的,是你没搞懂“Star”在底层到底怎么读的。很多新手卡在“Star怎么读”这个看似简单的概念上,其实这里藏着后端数据流的关键。…

作者头像 李华
网站建设 2026/9/22 22:51:51

搞定sim卡号码逻辑:从入门到精通的实战避坑指南

搞定sim卡号码逻辑:从入门到精通的实战避坑指南 看了一堆教程还是不会写项目?别急着骂人,问题出在你只背了API,没懂业务。做全栈开发,尤其是涉及物联网、房建工程数字化管理时, sim卡号码…

作者头像 李华
网站建设 2026/9/22 22:51:43

S-Line图解原理:3步搞定面试必问底层逻辑

S-Line图解原理:3步搞定面试必问底层逻辑 刚学完 Python 或 Java 语法,对着 IDE 敲代码挺顺,但一到面试问“数据怎么在组件间传递”或者“状态管理底层机制”,脑子瞬间空白。这不是你笨,是没人把你从“语法执行”拽进“架构思维”。S-Line(S-曲线)看似简单,却是前端渲染、后端异…

作者头像 李华