并发编程手册

速查向 · 概念辨析与场景设计的思路要点(不附代码,重在设计思想与解题方向)

1. 基础概念辨析

1.1 进程 / 线程 / 协程

Q:说说三者的区别与适用场景。 进程是资源分配的基本单位,有独立地址空间,切换开销最大(涉及页表切换、TLB 失效)。
线程是 CPU 调度的基本单位,同进程线程共享地址空间,切换只保存寄存器,开销较小,但共享内存需同步。
协程是用户态的轻量执行体,由语言运行时/库调度,不经过内核,切换开销最小(纳秒级),适合高并发 IO 密集。
要点:进程隔离性最好、线程共享方便但需锁、协程最轻量但需语言支持。

1.2 并行 vs 并发

Q:并发和并行的区别? 并发(Concurrency)是逻辑上同时处理多个任务,通过时间片轮转,可以是单核。
并行(Parallelism)是物理上同时执行,必须多核。
比喻:并发是一个人交替做多件事,并行是多人同时各做一件事。并发是结构,并行是执行。

1.3 上下文切换

Q:什么是上下文切换?开销来自哪里? 保存当前任务的执行状态(寄存器、程序计数器、栈指针),恢复下一个任务的状态。
开销来源:① 寄存器/栈的保存恢复;② 进程切换还需切换页表导致 TLB 失效;③ 缓存局部性被破坏(cache 失效)。
如何减少:减少线程数、用协程、无锁编程、批量处理、绑核(CPU affinity)。

1.4 死锁

Q:死锁的四个必要条件?如何预防? 四个必要条件(缺一不可):
① 互斥:资源同一时刻只能被一个持有;
② 持有并等待:持有资源同时等待其他资源;
③ 不可剥夺:资源不能被强行抢走;
④ 循环等待:存在等待环路。
预防(破坏任一条件):① 尽量用共享/无锁结构;② 一次性申请全部资源;③ 允许超时抢占/释放;④ 统一加锁顺序(最实用)。
避免 vs 检测:银行家算法(避免)、资源分配图找环(检测)。工程上更常用加锁顺序 + 超时。

1.5 活锁与饥饿

Q:活锁和饥饿是什么? 活锁:线程没阻塞,一直在"礼让/重试",但始终无法推进(如两个线程同时退让)。
饥饿:低优先级线程长期拿不到资源,被高优先级线程持续抢占。
解决:活锁用随机退避;饥饿用公平锁(FIFO)或优先级老化。

2. 锁与同步机制

2.1 乐观锁 vs 悲观锁

维度悲观锁乐观锁
思想假设一定会冲突,先加锁假设不冲突,提交时校验
实现互斥锁、行锁、SELECT FOR UPDATE版本号、CAS、时间戳
适用写多读少、冲突激烈读多写少、冲突少
代价加锁开销、可能阻塞失败需重试、ABA 问题
ABA 问题:值从 A→B→A,CAS 认为没变。解决:加版本号或时间戳。

2.2 CAS 与原子操作

Q:CAS 的原理和缺点? 原理:比较内存值与期望值,相等则更新为新值,整个操作由 CPU 指令(如 CMPXCHG)保证原子。
缺点:① ABA 问题;② 自旋开销大(高竞争下空转浪费 CPU);③ 只能保证一个变量的原子性(多个可用 AtomicReference 打包或加锁)。

2.3 自旋锁 / 读写锁 / 可重入锁

2.4 无锁编程

Q:如何实现一个无锁队列?思路是什么? 核心:用 CAS 操作头尾指针代替加锁。
单生产者-单消费者(SPSC):最简单,生产者和消费者各操作一端,无需 CAS,只需保证读写指针的可见性(内存屏障)。
多生产者:入队需 CAS 竞争尾指针,失败则重试;难点是安全的回收节点(否则 ABA / 悬空指针)。
关键技术:CAS、内存屏障(acquire/release)、内存回收(引用计数、Hazard Pointer、RCU)。
陷阱:"无锁"不等于"无等待",多生产者下仍可能自旋重试(lock-free 但非 wait-free)。

3. 线程池与并发控制

3.1 线程池设计

Q:设计一个线程池需要考虑哪些点? ① 核心/最大线程数:IO 密集型可设大(2×核数以上),CPU 密集型设核数+1;
② 任务队列:有界(防 OOM)还是无界(可能积压);
③ 拒绝策略:抛异常 / 丢弃 / 丢弃最旧 / 由提交者执行(降级);
④ 线程复用与保活:空闲超时回收;
⑤ 优雅关闭:停止接收新任务 + 等待已提交任务完成;
⑥ 监控:队列长度、活跃线程数、拒绝次数;
⑦ 命名与隔离:不同业务用不同池,避免互相拖垮。

3.2 限流算法

算法思路特点
固定窗口每秒计数,超限拒绝简单,但有临界突刺问题
滑动窗口按时间粒度细分统计平滑,内存开销略大
漏桶请求入桶,匀速流出绝对平滑,不能应对突发
令牌桶匀速生成令牌,拿牌才放行允许一定突发,最常用
Q:分布式限流怎么做? 用 Redis + Lua 保证"读计数 + 判断 + 自增"的原子性;或基于令牌桶的 Redis 实现。
注意时间同步和 Redis 单点压力,可本地限流 + 集群限流两级配合。

3.3 生产者-消费者

Q:实现生产者消费者模型,关键点是什么? 三要素:① 缓冲区(有界队列,满则阻塞生产者,空则阻塞消费者);② 同步机制(互斥锁 + 条件变量,或用阻塞队列);③ 停止信号(关闭通知消费者退出,避免永久阻塞)。
要点:用 while 而非 if 判断条件(防虚假唤醒);先获取锁再判断条件;停产后要唤醒所有等待者。

3.4 多线程按序执行

Q:如何让多个线程按指定顺序执行? 思路一(信号量):每个线程配一个信号量,A 完成后 release B 的信号量,依次传递。
思路二(条件变量):共享一个"当前轮到谁"的变量,不满足则 wait,满足则唤醒下一个。
思路三(管道/channel):用阻塞队列串联,前一个的输出作为后一个的输入(流水线)。
扩展:交替打印——两个线程分别 wait/notify,或用两个 channel 互相传递"令牌"。

3.5 超时与取消

Q:如何实现任务超时控制与取消? 核心思路:传递一个"取消信号"向下传播。
① Go:context.WithTimeout 派生子 context,内部通过 Done() channel 通知;
② Java:Future.get(timeout) 或 CompletableFuture.orTimeout;
③ 通用:用 select/poll 配合超时时间等待结果 channel。
要点:取消要逐层传递(父取消则子全取消);资源要及时释放(defer/cancel);不可取消的阻塞(如某些系统调用)需配合强制中断或监控。

3.6 单例与初始化

Q:如何实现线程安全的单例? ① 饿汉式:类加载即创建,简单但可能浪费(不用也建);
② 懒汉 + 双检锁(DCL):注意实例字段要 volatile,防止指令重排导致拿到半初始化对象;
③ 静态内部类:利用类加载机制保证线程安全与延迟加载(推荐);
④ 枚举:天然防反射与序列化破坏(Java 最佳实践);
⑤ Go 用 sync.Once,简洁且保证只执行一次。

4. 内存模型与可见性

4.1 内存可见性

Q:为什么多线程会看到"过期"的变量值? 每个 CPU 有缓存,写操作可能只更新到本地缓存,其他核看不到;编译器/CPU 还会重排序指令。
解决:① 内存屏障/volatile(强制刷新到主存、禁止重排);② 加锁(锁的释放-获取本身带屏障语义);③ 原子操作。

4.2 happens-before

Q:什么是 happens-before 原则? 定义可见性的偏序关系:如果 A happens-before B,那么 A 的结果对 B 可见。
主要规则:① 程序顺序(单线程内);② 监视器锁(解锁 hb 后续加锁);③ volatile 写 hb 后续读;④ 线程启动/终止(start hb 线程内所有操作);⑤ 传递性。

4.3 伪共享(False Sharing)

Q:什么是伪共享?如何避免? 多个线程修改同一缓存行(通常 64 字节)中的不同变量,导致缓存行频繁失效、性能暴跌。
解决:缓存行填充(padding,把变量间隔到不同缓存行);Java 用 @Contended 注解;Go 中手动填充。
场景:高并发计数器数组、无锁队列的 head/tail 指针。

5. 场景设计题

5.1 设计题:并发安全的高性能缓存

Q:如何设计一个支持高并发的本地缓存? ① 分片(Sharding):按 key 哈希拆成 N 个 map,各自加锁,降低锁竞争;
② 读写锁:读多写少用 RWMutex;
③ 淘汰策略:LRU/LFU/TTL,注意淘汰也要加锁,可做惰性删除;
④ 单飞(SingleFlight):同一 key 的并发回源请求合并成一次;
⑤ 防缓存击穿:热点 key 不过期 + 异步刷新;
⑥ 统计:命中率、淘汰次数,用原子计数避免锁。

5.2 设计题:多线程统计 / 计数

Q:如何高效统计海量请求的计数? ① 不要所有线程竞争一个原子变量(伪共享 + 单点竞争);
② 分段计数(LongAdder 思想):每个线程/分片维护独立计数,读取时求和;
③ ThreadLocal 累加:线程本地累加,定期汇总;
④ 注意缓存行填充,避免伪共享。

5.3 设计题:限流 + 熔断 + 降级

Q:如何设计一个服务的高可用防护体系? 分层:
① 限流:入口挡掉超量请求(令牌桶/滑动窗口),保护自身;
② 熔断:调用下游持续失败时快速失败,不再打过去(三态:关闭/打开/半开);
③ 降级:核心功能受损时返回兜底数据(缓存旧值、默认值);
④ 隔离:线程池/信号量隔离,避免一个依赖拖垮全部;
⑤ 超时:所有远程调用必须设超时,防线程耗尽;
⑥ 重试:注意幂等与退避,避免重试风暴。

5.4 高频手写题思路

题目核心思路
两个线程交替打印 1-100两个信号量/条件变量互相唤醒
三个线程循环打印 ABC三个信号量构成环形令牌传递
生产者消费者有界队列 + 互斥锁 + 非空/非满条件变量
多线程累加求和的正确写法分段计算 + 结果汇总,避免共享变量竞争
实现一个阻塞队列环形数组/链表 + 锁 + 两个条件变量
实现读写锁读计数 + 写标志 + 条件变量
限流器(令牌桶)记录上次补充时间,按时间差补充令牌
并发安全的单例双检锁 + volatile,或 Once/静态内部类
定时任务调度器最小堆存任务 + 等待唤醒 + 时间轮加速

5.5 如何排查并发问题

Q:线上出现并发 bug,你怎么定位? ① 看现象:是数据错乱(竞态)、卡死(死锁)、还是性能骤降(锁竞争)?
② 抓栈:死锁看线程栈找相互等待的锁;CPU 高看热点方法;
③ 竞态检测:Go 用 -race,Java 用压力测试 + 静态分析;
④ 复现:加大并发、拉长临界区、调整时序;
⑤ 日志:打印关键状态与线程标识,注意日志本身的线程安全;
⑥ 修复后回归:压测验证,确认无锁竞争与死锁。

6. 速查清单

主题一句话答案
进程/线程/协程独立地址空间 / 共享内存 / 用户态最轻量
并发 vs 并行逻辑同时 vs 物理同时(需多核)
死锁四条件互斥、持有等待、不可剥夺、循环等待
预防死锁统一加锁顺序、一次性申请、超时释放
悲观 vs 乐观先加锁写多 / CAS 校验读多
ABA 问题加版本号或时间戳
CAS 缺点ABA、自旋开销、只能保证单变量
线程池要点核心/最大线程、队列、拒绝策略、优雅关闭
CPU 密集线程数核数 + 1
IO 密集线程数核数 × 2 以上,视阻塞比例
限流算法固定/滑动窗口、漏桶、令牌桶
生产者消费者有界队列 + 锁 + 非空非满条件变量,while 判断
超时取消context/Future + 逐层传递取消信号
线程安全单例DCL + volatile / 静态内部类 / 枚举 / Once
可见性volatile / 内存屏障 / 锁
happens-before锁释放-获取、volatile 写-读、start/join
伪共享同缓存行不同变量互相失效 → 缓存行填充
分段计数LongAdder 思想,避免单点竞争
熔断三态关闭 → 打开 → 半开