第 08 期:线程、锁与调度——为什么会卡?怎么解决?
1)面试常问的问题
- Java 的线程和操作系统的线程是什么关系?
- 什么是“内核态”和“用户态”?为什么切换它们会慢?
synchronized的锁为什么会从“偏向”变成“轻量级”,最后变成“重量级”?- 线程切换为什么贵?怎么减少?
2)先搞清楚问题本质
想象一下:
- 线程就像餐厅里的服务员,每个服务员可以同时处理一桌客人。
- 锁就像厨房的“唯一菜刀”,谁拿到刀才能切菜,避免两个人同时切乱了。
- 调度就像经理安排哪个服务员先上哪桌。
约束条件:
- 多核 CPU = 多个灶台,可以并行做菜。
- 每个线程要抢资源(临界区),不能乱抢。
- 系统希望响应快,不要让客人等太久。
成本在哪里?
- 上下文切换:服务员换桌子,要带上自己的笔记(寄存器、栈),很耗时。
- 锁竞争:大家都想用菜刀,等刀的时间长。
- 自旋/阻塞:等刀时,有人干站着(自旋),有人去休息室(阻塞)。
- 内存屏障:保证大家看到的是最新菜单,不是旧的。
结论:
- 如果“切菜”很快,且竞争不大 → 用轻量级锁,占用CPU稍等一下,切换快(自旋一下就好)。
- 如果竞争激烈或切菜时间长 → 用重量级锁,不占用CPU,切换缓慢(去休息室等通知)。
- 能不用锁就不用锁(比如每人一把刀)。
3)锁升级路径(JDK 8+ 简化版)
- 偏向锁:如果只有一个人用刀,直接记住他,下次不用检查。
- 轻量级锁:来了第二个人,开始抢刀,抢不到就占用CPU等(自旋)。
- 重量级锁:人太多,自旋浪费 CPU,干脆让大家去休息室,等经理通知(操作系统调度)。
4)代码示例
public class ContentionDemo {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) throws Exception {
Runnable task = () -> {
for (int i = 0; i < 100_000; i++) {
synchronized (lock) { counter++; }
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
long start = System.nanoTime();
t1.start(); t2.start();
t1.join(); t2.join();
long end = System.nanoTime();
System.out.printf("counter=%d, time=%.2f ms\n", counter, (end - start)/1e6);
}
}
这段代码模拟两个线程抢同一把“菜刀”,看看耗时。
5)速答卡(面试必背)
- Java 线程 ≈ 操作系统线程,调度由 OS 完成。
- 锁升级:偏向 → 轻量级(CAS + 自旋) → 重量级(阻塞)。
- 降低切换:缩短临界区、减少共享写、用无锁或分区化。