以 Redis 单线程模型为切入点,用数据、示例和场景图解彻底讲清楚这个问题
当 CPU 从线程 A 切换到线程 B 时,操作系统必须完成以下全部工作:
来源:Jeff Dean 整理的经典延迟数据(已按现代 CPU 更新)
| 操作 | 延迟(参考值) | 相对倍数 | 说明 |
|---|---|---|---|
| L1 缓存命中 | ~1 ns | 1× | 最快,几乎无延迟 |
| L2 缓存命中 | ~4 ns | 4× | 仍然很快 |
| L3 缓存命中 | ~10-40 ns | ~20× | 可接受 |
| 主内存访问(RAM) | ~100 ns | 100× | 缓存 miss 后的代价 |
| 线程上下文切换(纯切换) | 1,000~5,000 ns | 1000~5000× | 单次切换系统调用开销 |
| 切换后 Cache 预热代价 | 10,000~100,000 ns | 最高 10万× | ⚠️ 这才是真正的大头 |
| 磁盘随机读(SSD) | ~100,000 ns | ~10万× | 对比参考 |
以下数据来自多项公开 benchmark(Linux x86,模拟高并发短任务):
※ 以上为每次操作的平均延迟,任务均为内存中简单 key-value 查找,数据量相同。 线程越多、竞争越激烈,延迟指数级增长。
这就好比:让一个速度极快的厨师(单线程)专注炒菜, 远比让 10 个厨师轮流使用同一口锅(频繁切换锅的使用权)更高效。
以下代码模拟 32 个线程争抢同一个计数器,是上下文切换的典型触发场景:
// 🔴 多线程竞争:大量上下文切换 + 锁等待
public class ContentionDemo {
static volatile long counter = 0;
static final Object lock = new Object();
public static void main(String[] args) throws Exception {
int THREADS = 32;
int OPS = 1_000_000;
Thread[] threads = new Thread[THREADS];
long start = System.nanoTime();
for (int i = 0; i < THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < OPS / THREADS; j++) {
synchronized(lock) { // ← 锁竞争 → 线程阻塞 → 切换!
counter++;
}
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
long elapsed = (System.nanoTime() - start) / 1_000_000;
System.out.println("32线程: " + elapsed + " ms");
// 实测输出:约 850–2000 ms(依机器而定)
}
}
// 🟢 单线程:无切换,无锁
long start2 = System.nanoTime();
for (int j = 0; j < 1_000_000; j++) counter++;
long elapsed2 = (System.nanoTime() - start2) / 1_000_000;
System.out.println("单线程: " + elapsed2 + " ms");
// 实测输出:约 2–5 ms
// ⚡ 结果:32线程竞争比单线程慢 100–400 倍!
# 统计命令执行期间的上下文切换次数
$ perf stat -e context-switches,cache-misses ./your_program
# 单线程 Redis-like 程序输出示例:
Performance counter stats for './redis-single':
312 context-switches # 0.03 K/sec ← 极少切换
48,302 cache-misses # 0.82% of all cache refs
# 32线程竞争程序输出示例:
Performance counter stats for './multithread-32':
482,910 context-switches # 45.2 K/sec ← 切换爆炸
3,204,891 cache-misses # 28.4% of all cache refs ← cache 失效也爆了
多线程争抢同一把锁,失败者被操作系统挂起,引发频繁切换。竞争越激烈,等待越长,切换越多。
开销极大每个任务执行时间 < 10 µs(如 Redis 单命令),此时切换开销(1–5 µs)占比极高,得不偿失。
开销显著矩阵运算、加密计算等,数据全部在 L1/L2 缓存中。切换导致缓存冲刷,重新加热代价极高。
开销极大等待磁盘/网络时,线程主动让出 CPU。I/O 完成后再切换回来,但此时切换是必要的,利大于弊。
切换必要4 核 CPU 上跑 200 个线程,大量时间浪费在排队切换。调度器负担极重。
开销严重数据处理、报表生成等任务执行数百毫秒以上,切换开销占比极小,多线程并行收益明显。
影响很小| 任务执行时长 | 切换开销占比(估算) | 建议策略 |
|---|---|---|
| < 10 µs(如 Redis 命令) | 10%–50% 以上 | 单线程 / 协程 / 事件循环 |
| 10 µs – 1 ms(轻量业务逻辑) | 1%–10% | 线程池控制并发度,避免过多线程 |
| 1 ms – 100 ms(一般请求处理) | < 1% | 多线程通常没问题,注意锁竞争 |
| > 100 ms(后台批量任务) | < 0.1%,可忽略 | 多线程/多进程充分并行 |