字节面试原题 Java面试直接过08 | 线程、锁与调度——程序为什么会卡?怎么解决?

35 阅读2分钟

第 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 + 自旋) → 重量级(阻塞)。
  • 降低切换:缩短临界区、减少共享写、用无锁或分区化。