虾皮研发一面 国庆也能收到感谢信了
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- Agent 会不会依赖 reference?会不会覆盖不到边缘场景?
- 怎么评测召回的准确率?测试集数量是多少?
- 平台的日活量大概是多少?
- 新的模型出来后怎么适配之前的 Agent 项目?这对 Agent 设计有什么影响?
- 在浏览器里输入一个网址,之后发生了什么?
- TCP 的拥塞控制是怎样的?在当前网络带宽很大的情况下它有什么局限性,需要怎么调整?
- 进程在一个地址写了数据,另一个进程在没有共享内存的情况下能读到这个数据吗?应该怎么让另一个进程读到?
- 两个进程希望通过共享内存通信,传输 1KB 的数据应该怎么做?
- 自旋锁和互斥锁有什么区别?为什么不都用自旋锁?
- 有一个接口服务 QPS 比较高,每秒大概 100 个请求进来,需要统计这个接口每秒的总调用次数。架构上是 100 个工作线程处理请求,另外有一个单独的统计线程负责汇总所有线程的计数,你会怎么设计这个计数结构?如果这个服务是分布式集群部署的,计数数据会被频繁修改,同时需要保证分布式环境下的数据一致性,你会怎么调整方案?
- 现在有 100 个工作线程,每秒总共产生一万次请求,其中绝大多数都是调用 config 的 get 方法读取配置,大概每秒一万次读操作,而配置的 update 更新频率很低,每十分钟才执行一次。针对这种典型的读多写少场景,你会怎么设计这个配置模块?
- 读写锁的底层实现原理是什么?传统读写锁在写锁释放后需要向所有等待的读线程广播唤醒信号,这个广播开销在高并发下比较明显,有没有比传统读写锁更好的优化方法?
- 如果是在多核 CPU 架构下,跨 CPU 核心的缓存同步和锁广播开销会更突出,针对这种多核 CPU 之间的广播问题,你会怎么设计优化?
- 更泛化地讲,针对多线程运行在多核 CPU 的场景,你做并发数据结构设计的核心思路是什么?
- 如果配置的数据大小不确定、可能会比较大,你刚才提到的每个线程维护一份配置副本的方案内存开销会很大,这个问题要怎么解决?
- DAG 怎么做拓扑排序,要按顺序列出来?DAG 只有一个入度为 0 和出度为 0 的节点,怎么找图里从入口到出口有多少条不同路径?
《参考解析》
-
TCP 拥塞控制在大带宽下的局限:慢启动从很小的拥塞窗口开始指数增长,带宽时延积(带宽 × RTT)越大,爬到能跑满带宽的窗口所需时间越久,短连接几乎全程都在慢启动里,跑不出带宽。AIMD 收到丢包就把窗口减半,在无线或有随机丢包的链路上会把非拥塞丢包当成拥塞,窗口长期上不去。调整方向有几层:把初始窗口调大(RFC 6928 的 10 个 MSS)、换成 CUBIC 或 BBR 这类不只把丢包当信号的算法、开启窗口缩放与 SACK、调大 socket 收发缓冲区,以及在上层减少往返次数(连接复用、多路复用、批量请求)。回答时把「为什么大带宽反而吃亏」讲清楚——瓶颈从带宽变成了 RTT 和丢包判定。
-
自旋锁与互斥锁的取舍:互斥锁拿不到就睡眠让出 CPU,适合临界区较长、竞争激烈的场景;自旋锁拿不到就在原地忙等,不进内核调度,适合临界区极短且持锁时间可预期的场景,省掉两次上下文切换。不能全用自旋的原因是:单核或线程数多于核数时,自旋者占着 CPU 不让持锁者跑,纯属空烧;临界区一长,等待时间直接变成 CPU 空转;另外自旋期间如果持锁线程被抢占或发生缺页,等待会被无限放大。工程上多用「先自旋几次再挂起」的自适应锁,内核里的 mutex 就有 optimistic spinning。
-
100 个线程的高频计数怎么设计:核心是消除共享写热点。单机方案是让每个线程在自己的缓存行上累加(用 padding 或
@Contended避免伪共享),读的时候再汇总;或者直接用LongAdder,它内部就是 Cell 数组分散热点、只在求和时聚合,比AtomicLong的单点 CAS 自旋扩展性好得多。改成分布式集群后,别让 100 个节点对同一个 Redis key 做 INCR——热点 key 加跨网络往返会成为新瓶颈;做法是节点本地按时间窗聚合、定时批量上报,汇总侧把数据打散到多个分片 key 或按时间分桶再求和。对统计类指标可以接受最终一致加定期对账,比强一致便宜得多,这个取舍要主动说出来。 -
读多写少的配置模块:思路是让读路径完全不加锁。用一份不可变配置对象加原子引用,读线程只读引用、拿到的必然是一个完整版本,写线程构造新对象后一次性替换引用即可,读侧无阻塞无计数器争用;再挂一个版本号或做 copy-on-write,替换和回滚都可控。传统读写锁的短板正好在这里——读锁也要改共享的计数器(同样有缓存行争用),写锁释放还要广播唤醒所有等待的读线程,读越多这两处越贵。所以高并发读场景常用
StampedLock的乐观读或纯不可变加引用替换。 -
多核下的缓存同步与并发结构设计:真正的开销来自缓存一致性协议——一个核写过的缓存行在其他核上都要失效,谁读谁要重新拉取,锁释放还得广播唤醒。设计思路按优先级排:能不共享就不共享,数据分片到线程本地(thread-local、per-CPU 变量)只在必要时聚合;必须共享时按缓存行隔离,避免伪共享;把共享写改成批量或追加,降低写频率;用乐观读(版本号加校验)替代加锁读,把同步成本压到一次校验。数据量大到不能每线程一份副本时,就只共享索引或句柄,实际内容按需分页加载——这正是第 15 问的标准答法。
-
共享内存与进程间通信:进程各有独立虚拟地址空间,A 在自己地址写的数据 B 读不到,即使物理页复用也不行。要让 B 读到,必须把同一段物理内存映射进两个进程:System V 的
shmget/shmat、POSIX 的shm_open加mmap,或者匿名mmap(MAP_SHARED)配合fork。只传 1KB 这种小数据,共享内存的映射与同步开销未必划算,管道、Unix domain socket 或消息队列更简单;真要用共享内存,注意它本身不提供同步,必须配信号量或 futex,还要处理进程崩溃后的残留清理与生命周期管理。 -
DAG 拓扑排序与路径计数:拓扑排序用 Kahn 算法——统计各点入度,入度为 0 的先入队,逐个出队并把指向的节点入度减一,减到 0 就入队,出队顺序即拓扑序;也可以用 DFS 后序取反。路径条数在拓扑序上做动态规划:
dp[v]等于所有前驱dp[u]之和,入口节点置 1,出口节点的值就是答案。这个递推只有在拓扑序上才成立,所以必须先排序再计数;入度、出度都为 0 的节点各只有一个,说明图是单入口单出口,不需要额外处理多源多汇。