第4章 并发编程与并行集合优化
核心思维:并发优化不是“加线程”就快,而是减少共享可变状态、降低锁竞争、利用数据并行。大厂面试重点考察你对 Future/Promise 模型的理解、并行集合的底层切片机制、以及线程池参数调优经验。
公司侧重点 & 面试官画像
| 公司类型 | 并发考点侧重 | 常见场景 |
|---|---|---|
| 字节跳动/美团 | 高并发 + 并行集合调优 + Future 超时处理 | 任务编排、大数据处理管道 |
| 阿里/腾讯 | Scala 与 Akka 模型、Promise 实现原理 | 微服务网关、流计算引擎 |
| 外企/金融 | JVM 线程池参数、死锁分析、ParSeq 内部 Splitter | 实时风控、交易撮合系统 |
volatile、Atomic、Lock 对比。
回答框架概述(三步法)
- 定位本质:区分“计算密集型”vs“IO密集型”,选择数据并行还是任务并行。
- 技术与权衡:对比
Future/ParSeq/Actor;说明线程池大小公式(N_CPU+1 或 IO 型放宽)。 - 验证与优化:使用
JProfiler或JMH做基准测试,重点观察 CPU 利用率 & 锁冲突。
🎯 问答 1:Future 与 Promise 的关系与性能陷阱
面试问题:Scala 中 Future 和 Promise 有什么区别?请描述一个因滥用 Future 导致性能下降的例子。
考察点:是否理解 Future 是只读占位符、Promise 是写入端;能否识别隐式线程池过载风险。
回答框架:
- 明确角色:
Future消费结果,Promise生产结果,两者一一配对。 - 指出常见陷阱:在循环内创建
Future而不复用线程池 → 导致线程池队列积压 + 上下文切换爆炸。 - 给出优化:使用
ExecutionService自定义线程池 + 限制并发数。
示例回答:
val pool = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4))
val tasks = (1 to 1000).map(i => Future { compute(i) }(pool))
Future.sequence(tasks).map(_.sum)(pool)
控制并发度为4,避免隐式 ForkJoinPool 撑爆。
可能追问:如果其中一个 Future 抛出异常,整体结果如何处理?Future.sequence 与 Future.traverse 的区别?
Future 是同步的”或混淆 Await 用法。切忌在 onComplete 里做阻塞操作(如 Await.result)。
🎯 问答 2:并行集合 ParSeq 的分片机制与数据竞争
面试问题:ParSeq 的 .par 是如何实现并行化的?如何避免并行操作中的数据竞争?
考察点:是否了解 Splitter 接口、TaskSupport 配置;能否分析 var 在并行流中的问题。
回答框架:
- 底层通过
Splitter将集合拆分为不重叠子任务,StealingTask实现工作窃取。 - 数据竞争:共享
var或mutable.ArrayBuffer导致不确定结果。 - 建议使用
aggregate/fold等纯函数操作,或原子累加器LongAdder。
示例回答:
“ParSeq 将集合分成多个分区,每个分区由一个 ForkJoinTask 处理。我曾在统计用户点击量时使用了 var sum += 1 造成结果偏小,改为 aggregate(0L)(_ + _, _ + _) 后正确。”
可能追问:ParSeq 的线程池如何指定?ThreadLocal 在并行集合中是否安全?
ParSeq 自动线程安全”。实际 foreach 内部直接修改外部状态不报错但结果错误。
🎯 问答 3:线程池调优与 Akka Actor 对比
面试问题:请对比 ForkJoinPool 与 ThreadPoolExecutor 在 Scala 并发下的适用场景,并给出 Akka Actor 的优缺点。
考察点:理解两种线程池的工作窃取 vs 有界队列;是否在实际项目中用过 Actor 解决状态共享问题。
回答框架:
- ForkJoinPool 适合计算密集型任务分裂(并行集合默认),ThreadPoolExecutor 适合 IO 密集型(定制核心/最大线程)。
- Akka Actor 通过邮箱隔离状态,避免显式锁;缺点:调试困难、消息丢失需超时机制。
示例回答:“在 IO 密集的爬虫项目中,我使用 ThreadPoolExecutor 设置 200 核心线程,避免 ForkJoinPool 的 work-stealing 浪费。对于共享计费状态,改用 Akka Actor 后消除了死锁。”
可能追问:如何监控 ForkJoinPool 的队列深度?scala.concurrent.blocking 的作用是什么?
ThreadPoolExecutor.getQueue.size())。