线程调度与优先级
本教程共 100 篇 · 第 96 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
96. 线程调度与优先级
本节目标:理解抢占式调度的本质,会用优先级、yield 和守护线程,同时知道它们的可靠边界。
抢占式调度
线程数量几乎总是超过 CPU 核心数,所以必须有人决定「此刻让谁跑」。这个角色就是操作系统的调度器。
现代系统采用抢占式调度:调度器给每个线程分配一小段时间片,时间用完就强行切走,换下一个线程。线程自己说了不算。
这带来一个重要结论:你无法预测代码的执行顺序。
同一段多线程代码跑十次,可能有十种不同的输出。这不是 bug,是并发的固有特性。写代码时必须假设「任何时刻都可能被打断」。
优先级的取值
Java 允许给线程设置优先级,用意是提示调度器「这个线程比较重要,多分点时间」。
取值范围是 1 到 10,Thread 类定义了三个常量:
| 常量 | 值 | 说明 |
|---|---|---|
Thread.MIN_PRIORITY | 1 | 最低 |
Thread.NORM_PRIORITY | 5 | 默认 |
Thread.MAX_PRIORITY | 10 | 最高 |
新建线程的优先级默认继承自创建它的那个线程。main 线程是 5,所以在 main 里直接 new 出来的线程也是 5。
public class PriorityDemo {
public static void main(String[] args) {
Runnable task = () -> {
String name = Thread.currentThread().getName();
int p = Thread.currentThread().getPriority();
long count = 0;
for (int i = 0; i < 50_000_000; i++) {
count++;
}
System.out.println(name + "(优先级 " + p + ")完成");
};
Thread low = new Thread(task, "低优先级");
Thread high = new Thread(task, "高优先级");
low.setPriority(Thread.MIN_PRIORITY);
high.setPriority(Thread.MAX_PRIORITY);
low.start();
high.start();
}
}
```bash
多跑几次,你会发现高优先级**不一定**先完成。有时先,有时后,完全没准。
## 优先级只是建议
这是本节最重要的一句话:**优先级是给调度器的建议,不是命令。**
原因有三个。
第一,Java 的 10 个级别要映射到操作系统的级别,而各系统的级别数量不同。Windows 只有 7 个级别,映射时必然有多个 Java 优先级挤到同一个系统级别上。Linux 上普通线程的优先级设置甚至可能完全无效。
第二,现代调度器有动态调整机制。一个线程长期得不到执行,系统会主动提升它的优先级,防止「饿死」。你设的值会被覆盖。
第三,优先级影响的是「被选中的概率」,不是「执行的顺序」。高优先级线程只是更容易被挑中,不代表一定排在前面。
> [!WARNING]
> 绝对不要用优先级来控制业务逻辑的先后顺序。需要 A 在 B 之前完成,就用 `join()` 或者 `CountDownLatch`,这些才是确定的。用优先级碰运气,测试环境正常、生产环境翻车是迟早的事。
优先级的合理用途只有一个:给纯后台的低价值任务(比如日志归档、缓存预热)设成 `MIN_PRIORITY`,避免它们和主业务抢 CPU。
## yield:主动让一步
`Thread.yield()` 是个静态方法,含义是:当前线程愿意放弃剩下的时间片,请调度器重新挑一个线程来跑。
```java
public class YieldDemo {
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
Thread.yield(); // 每轮让一次
}
};
new Thread(task, "A").start();
new Thread(task, "B").start();
}
}
关于 yield() 有几点必须清楚。
它不会阻塞线程,线程状态仍然是 RUNNABLE,只是回到就绪队列重新排队。
它可能完全无效。调度器有权忽略这个请求,转头又把 CPU 分给同一个线程。
它不会释放锁。如果线程正持有 synchronized 锁,yield() 之后锁还在手上,别的线程照样进不去。
Tip实际开发里几乎用不到
yield()。想让出 CPU,Thread.sleep(1)的效果反而更确定。了解它主要是为了看懂别人的代码,以及应付面试。
sleep 和 yield 的区别
| 对比 | sleep(n) | yield() |
|---|---|---|
| 状态 | TIMED_WAITING | 仍是 RUNNABLE |
| 时长 | 至少睡 n 毫秒 | 立刻重新参与竞争 |
| 释放锁 | 不释放 | 不释放 |
| 可靠性 | 一定生效 | 可能被忽略 |
| 异常 | 抛 InterruptedException | 不抛异常 |
两者都不释放锁,这一点很关键。唯一会释放锁的是 Object.wait()。
守护线程
Java 的线程分两类:用户线程和守护线程。
区别只有一条:JVM 在所有用户线程都结束时退出,完全不管守护线程还在不在跑。
垃圾回收线程就是典型的守护线程。你的程序跑完了,它没必要继续存在。
public class DaemonDemo {
public static void main(String[] args) throws InterruptedException {
Thread daemon = new Thread(() -> {
int i = 0;
while (true) {
System.out.println("守护线程心跳 " + (++i));
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "心跳线程");
daemon.setDaemon(true); // 必须在 start() 之前设置
daemon.start();
Thread.sleep(700);
System.out.println("main 结束,JVM 即将退出");
}
}
```text
输出:
```text
守护线程心跳 1
守护线程心跳 2
守护线程心跳 3
守护线程心跳 4
main 结束,JVM 即将退出
那个 while (true) 死循环并没有阻止程序退出。main 一结束,JVM 发现只剩守护线程,直接收工。
setDaemon(true) 必须在 start() 之前调用,否则抛 IllegalThreadStateException。已经跑起来的线程改不了身份。
守护线程创建的子线程,默认也是守护线程,这个属性会传递下去。
守护线程的陷阱
守护线程被 JVM 强制终止时,不会执行任何善后代码。
finally 块不会跑,try-with-resources 不会关流,缓冲区里的数据直接丢失。
// 危险示范
Thread t = new Thread(() -> {
try (Writer w = new FileWriter("important.log")) {
while (true) {
w.write("重要数据\n");
Thread.sleep(100);
}
} catch (Exception e) {
// ...
}
});
t.setDaemon(true); // 一旦 JVM 退出,缓冲区数据全丢
t.start();
```bash
> [!WARNING]
> 守护线程里不要做文件写入、数据库操作、网络提交这类需要收尾的事。它适合的是纯粹的辅助任务:定时刷新缓存、采集监控指标、发心跳包——丢了也无所谓的那种。
需要保证完成的任务,应该用普通用户线程,让 JVM 等它跑完。
## 本节小结
- 抢占式调度决定线程执行顺序,代码不能依赖任何顺序假设。
- 优先级 1 到 10,默认 5,继承自创建它的线程。
- 优先级只是建议,受平台差异和动态调整影响,不可依赖。
- 控制先后顺序要用 `join()` 或同步工具,不要用优先级。
- `yield()` 让出时间片但不阻塞、不释放锁,且可能被忽略。
- 守护线程在用户线程全部结束时被强制终止。
- `setDaemon(true)` 必须在 `start()` 之前调用。
- 守护线程被终止时不执行 `finally`,不要用它做需要收尾的工作。