1. 并发模型概述:从单线程到多线程的演进
各位同学,咱们今天聊聊并发模型。说实话,我刚开始写程序那会儿,压根没想过"并发"这回事。那时候的单核 CPU,跑一个进程就挺满足。但后来,事情变了。
1.1 从单线程到多线程:为什么非变不可?
早期的服务端程序,就是一个 while 循环,挨个处理请求。你想想看,一个请求没处理完,后面的就得排队等着。这在用户量小的时候还行,一旦并发上来,响应时间直接爆炸。
我记得 2010 年左右,我参与过一个电商后台项目。上线第一天,用户一多,服务直接卡死。查了半天,发现是单线程处理请求,一个慢查询把整个线程堵住了。嗯,从那以后,我对"单线程瓶颈"有了刻骨铭心的认识。
演进路径其实很清晰:
- 单进程单线程:简单,但吞吐量极低。一个阻塞点拖垮全局。
- 多进程:每个请求一个进程,隔离性好,但进程创建销毁开销大,内存占用高。
- 多线程:共享内存,轻量级切换,成为主流。但随之而来的是竞态、死锁、上下文切换开销。
- 事件驱动/协程:用户态调度,更轻量,适合 I/O 密集型场景。
核心观点:从单线程到多线程,本质是用"复杂度"换"吞吐量"。没有银弹,只有权衡。
1.2 并发与并行的区别
这个问题,面试里被问烂了。但说实话,很多人还是搞混。我简单说:
- 并发(Concurrency):逻辑上的"同时"。多个任务在同一个时间段内交替执行。你吃着火锅唱着歌,虽然嘴和嗓子不能同时用,但宏观上你确实在"同时"做两件事。
- 并行(Parallelism):物理上的"同时"。多个任务在多个 CPU 核心上真正同时执行。你左手画圆右手画方,两个手各干各的。
用个图可能更直观:
个人经验:我在做高并发网关时,经常遇到"并发设计没问题,但并行效率上不去"的情况。原因往往是锁竞争太激烈,导致多个核心在"抢锁"上浪费了大量时间。这时候,并发模型选对了,但并行效果没出来。
1.3 为什么需要并发模型?
你可能会问:直接用线程不就行了?搞什么模型?
我刚开始也这么想。直到有一次,我接手一个遗留系统,里面线程满天飞,new Thread() 到处都是。结果呢?
- 线程数一多,上下文切换开销把 CPU 吃光了。
- 共享数据没保护,竞态条件导致订单金额算错。
- 死锁了,服务完全无响应,只能重启。
说白了,并发模型就是一套"管理并发任务的规则和模式"。它帮你回答几个关键问题:
- 任务怎么拆分?——是每个请求一个线程,还是用线程池复用?
- 数据怎么共享?——用锁?用消息传递?用无锁数据结构?
- 任务怎么调度?——抢占式还是协作式?
- 错误怎么处理?——线程挂了怎么办?任务超时怎么恢复?
没有模型,你就是在裸写并发。短期能跑,长期必崩。
避坑指南:我曾经在一个项目中,为了"性能",大量使用 synchronized 粗粒度锁。结果性能没上去,反而因为锁竞争导致吞吐量下降。后来改用读写锁 + 分段锁,才解决问题。记住:并发模型不是越复杂越好,而是越适合场景越好。
1.4 主流并发模型一览
咱们课程会深入讲解几种主流模型。这里先给个全景图:
| 模型 | 核心思想 | 典型代表 | 适用场景 |
|---|---|---|---|
| 线程池模型 | 预创建线程,任务队列分发 | Java ThreadPoolExecutor | 通用服务端,计算密集型 |
| 事件驱动模型 | 单线程事件循环,非阻塞 I/O | Node.js, Netty, Redis | I/O 密集型,高并发连接 |
| Actor 模型 | 消息传递,无共享状态 | Erlang, Akka | 分布式系统,高可用场景 |
| 协程模型 | 用户态轻量级线程,协作式调度 | Go goroutine, Kotlin coroutine | 高并发,微服务 |
| 数据流模型 | 数据驱动计算,流水线处理 | Apache Flink, Spark Streaming | 大数据实时处理 |
每种模型都有自己的哲学。线程池模型强调"资源复用",事件驱动模型强调"非阻塞",Actor 模型强调"隔离",协程模型强调"轻量"。没有绝对的好坏,只有是否匹配你的业务场景。
我的建议:初学者不要贪多。先把线程池模型吃透,因为它是最通用的基础。然后再去学事件驱动和协程。Actor 模型和数据流模型,等你遇到分布式场景再深入也不迟。
1.5 本章小结
咱们这一章,从单线程的局限讲到了多线程的演进,理清了并发和并行的区别,也解释了为什么需要并发模型。说白了,并发模型就是帮你"管好线程、用好资源、不出 bug"的一套方法论。