多线程上下文切换是什么意思?代价有多大?

#Java#面试八股

多线程上下文切换是什么意思?代价有多大?

结论先行:上下文切换(context switch)是 CPU 从一个执行流换到另一个执行流时,保存当前现场、恢复下一个现场的过程。它本身不干业务活,纯粹是开销。线程不是越多越好,根本原因就在这里。

1. 四类切换,代价差三个数量级

切换类型由谁触发保存/恢复的内容量级
进程切换内核调度器页表、寄存器、内核栈,可能刷新 TLB几微秒 ~ 毫秒
线程切换(同进程)内核调度器寄存器、程序计数器、栈指针约 1~10 微秒
模式切换系统调用、中断只切 CPU 特权级,不换执行流几百纳秒
协程切换用户态运行时少量寄存器 + 栈指针几十纳秒

面试里说"上下文切换很贵",通常指的是线程切换;说"系统调用贵",指的是模式切换。两者不是一回事。

2. 切换时 CPU 到底在干什么

[ 线程 A 运行 ] ──保存现场──▶ [ 恢复线程 B 现场 ] ──▶ [ 线程 B 运行 ]
                        └──── 切换窗口:CPU 不执行任何业务指令 ────┘

一次线程切换要做的事:

  1. 把 A 的通用寄存器、PC、SP 压到 A 的内核栈;
  2. 切到 B 的内核栈,恢复 B 的寄存器与 PC;
  3. 如果是跨进程,还要换页表、刷 TLB;
  4. 副作用:A 的缓存局部性被破坏,B 的指令和数据要重新从内存/LLC 加载。

第 4 点最容易被忽略,但往往是真实成本的大头:切换后前几千条指令的 cache miss 都算在切换头上。

3. 怎么观测

# 每 1 秒采样:cs 列是上下文切换总次数,r 列是运行队列长度
vmstat 1
#  procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#   r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa
#   8  0      0 812345  12000 980000    0    0     4    12  900 12000 30  8 60  2

# 指定进程:cswch/s 主动切换(等 IO/锁),nvcswch/s 被动切换(时间片用完被抢占)
pidstat -w -p 12345 1

判读要点:

现象含义
cs 极高(几万以上)且 CPU sy 占比高线程过多或锁竞争激烈,内核在忙着调度
r 长期大于核数有大量线程在等 CPU,说明线程数超过实际并行能力
cswch/s 高、nvcswch/s 低线程大多在等 IO 或锁,属于"自愿让出"
nvcswch/s 高线程在抢 CPU 时间片,通常是 CPU 打满
us 低但 cs 高忙于切换、没干正事,典型"线程加多了"

4. 减少无效切换的手段

// 1. 用有界队列控制排队量,避免线程数随流量无限膨胀
new ThreadPoolExecutor(8, 8, 0L, TimeUnit.MILLISECONDS,
        new ArrayBlockingQueue<>(500));   // 固定 8 线程,超出的排队或拒绝

// 2. 短任务批量合并,减少"提交一次跑一次"的调度次数
List<Order> batch = new ArrayList<>(200);
for (Order o : orders) {
    batch.add(o);
    if (batch.size() == 200) { flush(batch); batch.clear(); }
}

// 3. 临界区尽量短,锁粒度尽量小:锁竞争会引发大量主动切换
synchronized (this) { counter++; }        // 只保护这一行

实践中的四条经验:

  1. 线程数控制在核数的合理倍数内(CPU 密集≈核数,IO 密集按等待占比放大);
  2. 缩小锁的临界区,或用无锁结构(AtomicLong、LongAdder)替代;
  3. 批量处理,把 N 次调度合并成 1 次;
  4. IO 密集用协程/异步,把"阻塞等"变成"挂起让出",虚拟线程正是为此而生。

5. 常见追问

追问一:线程切换一定比进程切换便宜吗? 同进程内的线程切换不需要换页表、TLB 通常仍有效,所以便宜。跨进程切换要换地址空间,代价高一个量级。

追问二:为什么线程多了反而更慢? 两个原因叠加:一是切换开销线性增长;二是每个线程都要占用栈内存和内核结构,缓存里能装下的热点数据被摊薄。压测时经常能看到"线程数 200 时 QPS 反而低于 50",就是总开销吃掉了并行收益。

追问三:协程为什么便宜? 切换发生在用户态,不需要陷入内核、不换内核栈,运行时自己保存几个寄存器就够了。但协程不能消除阻塞本身,它只是把"线程被占住"变成"栈被挂起"。

追问四:上下文切换次数越多,说明性能越差吗? 不一定。IO 密集服务的切换次数天然很高(每次读 socket 都可能让出),只要 CPU 利用率高、r 不大、延迟正常,就属于健康状态。要看的是"切换多且没产出",不是切换多本身。

小结

上下文切换 = 保存现场 + 恢复现场 + 缓存失效。工程上只需记住三件事:线程数要有上限、锁的临界区要短、阻塞等待尽量换成协程挂起。

笔记加载中…