首页 / Java 入门教程 / 线程调度与优先级

Java 入门教程

线程调度与优先级

本教程共 100 篇 · 第 96 篇 · 更新于 2026-08-05 · 约 5 分钟阅读

JavaJava 入门教程线程优先级调度yield守护线程

96. 线程调度与优先级

本节目标:理解抢占式调度的本质,会用优先级、yield 和守护线程,同时知道它们的可靠边界。

抢占式调度

线程数量几乎总是超过 CPU 核心数,所以必须有人决定「此刻让谁跑」。这个角色就是操作系统的调度器。

现代系统采用抢占式调度:调度器给每个线程分配一小段时间片,时间用完就强行切走,换下一个线程。线程自己说了不算。

这带来一个重要结论:你无法预测代码的执行顺序

同一段多线程代码跑十次,可能有十种不同的输出。这不是 bug,是并发的固有特性。写代码时必须假设「任何时刻都可能被打断」。

优先级的取值

Java 允许给线程设置优先级,用意是提示调度器「这个线程比较重要,多分点时间」。

取值范围是 1 到 10,Thread 类定义了三个常量:

常量说明
Thread.MIN_PRIORITY1最低
Thread.NORM_PRIORITY5默认
Thread.MAX_PRIORITY10最高

新建线程的优先级默认继承自创建它的那个线程。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 等它跑完。

## 本节小结

- 抢占式调度决定线程执行顺序,代码不能依赖任何顺序假设。
- 优先级 110,默认 5,继承自创建它的线程。
- 优先级只是建议,受平台差异和动态调整影响,不可依赖。
- 控制先后顺序要用 `join()` 或同步工具,不要用优先级。
- `yield()` 让出时间片但不阻塞、不释放锁,且可能被忽略。
- 守护线程在用户线程全部结束时被强制终止。
- `setDaemon(true)` 必须在 `start()` 之前调用。
- 守护线程被终止时不执行 `finally`,不要用它做需要收尾的工作。