面灵AI→

拼多多客户端研发一面面经

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. 请做一下自我介绍。
  2. 实习项目是做什么的,介绍一下?
  3. 这个地图渲染主要用的什么技术?前端 JS 做地图渲染的技术有去了解吗?
  4. 你这边还做了哪些工作?有哪些觉得有意思的功能值得分享的?
  5. 你开发这个功能用的什么设计模式?
  6. 实验室项目里你主要工作是什么?有用到一些框架吗?
  7. Kotlin 的协程有用过吗?
  8. 蓝牙用的什么协议?蓝牙协议的知识有了解吗?
  9. MQTT 是你选型和搭建的吗?MQTT 主要用在什么场景?有什么特点?
  10. 为什么选择做这个个人项目?这里面有什么技术亮点吗?
  11. AI 是放在服务端的吗?客户端怎么和服务端交互的?
  12. 并发和并行有什么区别?
  13. 进程和线程有什么区别?
  14. TCP 三次握手的过程是怎样的?两次握手会怎么样?
  15. HTTP1.0 的长连接复用和 HTTP2.0 的多路复用有什么区别?
  16. Java 怎么实现同步的?
  17. synchronized 关键字写在方法前面和方法里面有什么区别?
  18. synchronized 关键字写在 static 静态方法前面会怎样?
  19. 手写代码:如何实现一个 LFU Cache?
  20. 手写代码:整数数组求最大的连续区间和?

《参考解析》

并发与并行、进程与线程,这两组概念要一次说清。 并发是「同时应对多件事」的能力,单核靠时间片轮转也能并发;并行是「同一时刻真正同时执行」,必须有多个物理核心。进程是资源分配的基本单位,拥有独立地址空间,隔离性强但创建和切换昂贵(页表、TLB、上下文);线程是 CPU 调度的基本单位,同一进程内的线程共享内存和文件描述符,只需要保存寄存器和栈,切换便宜、通信方便,代价是要自己处理同步问题。再补一句 Java 侧的现状:现代 JVM 的线程和 OS 线程是一对一映射,所以「几万个线程」是不现实的;真正的用户态并发要靠虚拟线程或者业务层的异步 IO 框架。

TCP 三次握手为什么不能少一次。 流程是客户端发 SYN(带上自己的初始序列号)→ 服务端回 SYN+ACK(带上自己的初始序列号并确认对方)→ 客户端回 ACK 确认。第三次的意义是让服务端确认「客户端确实收到了我的 SYN+ACK」——只握两次的话,服务端发完 SYN+ACK 就认为连接已建立并分配资源,但客户端可能压根没收到这条报文。更实际的危害是历史重复报文:网络里一个延迟很久的旧 SYN 到达服务端,两次握手下服务端会直接建立一个半开连接并占着资源,而客户端根本不认这条连接,这就是 SYN Flood 的资源消耗原理。四次是能省成三次的,因为服务端的 SYN 和 ACK 可以合并成一条发。

HTTP1.0 的长连接和 HTTP2.0 的多路复用不是一回事。 HTTP1.0 默认一个请求一个 TCP 连接,加了 Connection: keep-alive 之后连接可以复用,但同一个连接上同一时刻仍然只能跑一个请求,后发的请求必须等前一个响应回来——这是 HTTP 层的队头阻塞,浏览器只能靠对同一域名开 6 个左右并发连接来绕过。HTTP2.0 把请求和响应拆成带 stream id 的二进制帧,在一条 TCP 连接上交错发送、互不阻塞,真正做到一个连接并发多路;同时加了 HPACK 头部压缩和服务端推送。但 HTTP2.0 仍然跑在 TCP 上,丢一个包会阻塞这条连接上所有的流,所以才有基于 UDP 的 QUIC / HTTP3 来解决传输层队头阻塞。

synchronized 的差异本质是「锁的是谁」。 修饰实例方法时锁是 this,也就是当前实例对象——同一个类的不同实例各有一把锁,互不阻塞,如果业务上要保护的是跨实例的静态资源,这么写就是错的。修饰代码块时锁是括号里那个对象,粒度可以自己控制,实践中应该选一个不会被外部共用的私有对象,别用 String 常量或者包装类缓存对象当锁,否则别的代码可能拿到同一把锁产生意外竞争。修饰静态方法时锁是类的 Class 对象,全局唯一,等价于把整个类的调用串行化,同一个类的所有实例都来抢这一把锁。另外要点出 synchronized 是可重入的、非公平的,JDK6 之后有偏向锁到轻量级锁再到重量级锁的升级过程,简单场景下不比 ReentrantLock 差;需要超时、可中断、公平锁或多条件队列时才换 ReentrantLock。

LFU Cache 要做到 O(1),靠的是「哈希 + 频次桶 + 双向链表」三件事。 用一个 map<key, node> 做定位,node 里存 value、freq 以及前后指针;再用 map<freq, 双向链表> 把同一频次的节点串起来。get 命中后要把节点从原频次链表摘下、插入 freq+1 的链表头部,并维护全局的 minFreq。put 时若已满,就淘汰 minFreq 那条链表的尾部节点(同频次内部按最近使用排序,尾部就是最久未用的,所以 LFU 天然要叠一层 LRU 来打破平局),新节点放到 freq=1,minFreq 置为 1。面试里更出彩的是接着讲 LFU 的固有缺陷:历史热点会靠高 freq 长期占位,新数据几乎进不来;工程上要么做频次衰减,要么直接用 W-TinyLFU 那种「频率统计 + LRU 窗口 + 频率门控」的混合策略,Caffeine 就是这么做的。