- 发布日期
Java多线程(二):线程调度、常用方法与生命周期
- 作者
- 姓名
- 缘
- 社交账号
文章目录
前言
会创建线程只是第一步。一个程序里出现多个线程以后,我们还会遇到一系列问题:怎样知道当前代码由哪个线程执行?能不能让线程暂停一会儿?如何等待另一个线程执行结束?线程优先级越高是否就一定先执行?
本篇围绕 Thread 的常用方法展开,并解释守护线程、礼让线程、插入线程和线程生命周期。理解这些内容后,我们才能正确分析一个线程此刻为什么没有继续运行。
一、线程名称与当前线程
有意义的线程名称非常重要。发生异常或分析日志时,Thread-0、Thread-1 很难让人判断任务来源,而 order-save-1、image-upload-2 一眼就能表达职责。
public class ThreadNameDemo {
public static void main(String[] args) {
Thread task = new Thread(() -> {
Thread current = Thread.currentThread();
System.out.println("当前线程:" + current.getName());
});
task.setName("文件上传线程");
task.start();
System.out.println("主线程:" + Thread.currentThread().getName());
}
}
常用方法包括:
setName(String name):设置线程名称。getName():获取线程名称。Thread.currentThread():获取正在执行当前代码的线程对象。
如果没有主动命名,JVM 会为线程生成默认名称,但这只适合简单演示。实际开发中应尽量为线程或线程池设置可识别的名称。
二、sleep:让当前线程限时等待
Thread.sleep(long millis) 会让当前正在执行的线程休眠指定的毫秒数。
public class SleepDemo {
public static void main(String[] args) {
for (int i = 3; i >= 1; i--) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("倒计时被中断");
return;
}
}
System.out.println("开始!");
}
}
使用 sleep() 时应理解四个细节:
sleep()是静态方法,影响的是当前线程,而不是“调用这个方法的对象”。推荐始终写成Thread.sleep(...)。- 休眠时间到达只代表线程重新具备参与调度的资格,不代表它一定立即执行。
sleep()不会释放当前线程已经持有的锁。- 其他线程可以通过
interrupt()中断休眠,被中断的线程会收到InterruptedException。
捕获 InterruptedException 后重新调用 Thread.currentThread().interrupt(),可以恢复中断标记,让更上层代码知道线程曾被要求停止。学习示例里经常直接打印异常,但真实业务中不应该无条件吞掉中断信号。
三、线程优先级
Java 线程优先级范围是 1 到 10:
Thread.MIN_PRIORITY // 1
Thread.NORM_PRIORITY // 5
Thread.MAX_PRIORITY // 10
Thread t1 = new Thread(task, "低优先级线程");
Thread t2 = new Thread(task, "高优先级线程");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
优先级高只表示它可能获得更多调度机会,绝不等于“高优先级线程一定先运行或先结束”。Java 线程最终要映射到操作系统的调度机制,不同平台的处理也可能不同。因此,不要把业务正确性建立在优先级上。凡是必须保证的执行顺序,应使用 join()、锁、队列等明确的协调手段。
四、守护线程
Java 线程可以分为用户线程和守护线程。主线程以及我们默认创建的线程通常属于用户线程;守护线程用于为其他线程提供后台服务。
public class DaemonDemo {
public static void main(String[] args) {
Thread backup = new Thread(() -> {
int i = 1;
while (true) {
System.out.println("执行第 " + i++ + " 次后台备份");
}
}, "备份线程");
backup.setDaemon(true);
backup.start();
for (int i = 1; i <= 5; i++) {
System.out.println("用户线程执行:" + i);
}
}
}
当 JVM 中已经没有存活的用户线程时,JVM 可以结束运行,守护线程也会随之结束。它不会保证把剩余逻辑完整执行完,所以写文件收尾、保存订单、数据库提交等必须可靠完成的任务,不应该只依赖守护线程。
调用 setDaemon(true) 必须发生在 start() 之前,否则会抛出 IllegalThreadStateException。
五、yield:礼让线程
Thread.yield() 表示当前线程愿意暂时让出 CPU 使用机会,让调度器重新选择可运行线程。
public class YieldDemo {
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 1; i <= 20; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
Thread.yield();
}
};
new Thread(task, "线程A").start();
new Thread(task, "线程B").start();
}
}
yield() 只是一种提示,调度器可以接受,也可以忽略。礼让之后,当前线程仍然可能再次获得执行权。因此它不提供确定的先后顺序,也不能用于实现可靠的线程通信。
六、join:等待指定线程结束
如果当前线程必须等另一个线程做完后才能继续,可以调用目标线程的 join()。
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread download = new Thread(() -> {
System.out.println("开始下载更新包");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
System.out.println("更新包下载完成");
}, "下载线程");
download.start();
download.join();
System.out.println("开始安装更新包");
}
}
主线程调用 download.join() 后,会等待 download 线程终止,再继续打印“开始安装”。课程里把它形象地称为“插入线程”,但从行为上看,更准确的理解是:当前线程等待目标线程结束。
join(long millis) 可以设置最长等待时间。如果在时间到达前目标线程仍未结束,当前线程也可以继续向下执行。
七、线程的生命周期
从业务视角看,线程大致会经历创建、就绪、运行、阻塞或等待、结束等阶段。Java API 中的 Thread.State 则定义了六种 JVM 线程状态。
1. NEW
线程对象已经创建,但还没有调用 start()。
Thread thread = new Thread(task);
System.out.println(thread.getState()); // NEW
2. RUNNABLE
调用 start() 后,线程进入 RUNNABLE。Java 把操作系统层面的“就绪”和“正在运行”都归入这个状态,因此看到 RUNNABLE 并不能断定线程此刻正在占用 CPU。
3. BLOCKED
线程尝试进入 synchronized 保护的区域,却发现监视器锁正被其他线程占有,于是进入 BLOCKED。它等待的是“锁”。
4. WAITING
线程无限期等待另一个线程执行某个动作。例如调用无超时参数的 Object.wait() 或 Thread.join(),都会进入 WAITING。它需要被唤醒或等待的目标线程结束,才可能继续运行。
5. TIMED_WAITING
线程在限定时间内等待,例如调用 Thread.sleep(1000)、Object.wait(1000) 或 Thread.join(1000)。时间到达后,它会重新进入可运行范围。
6. TERMINATED
run() 执行完毕,或者因为未处理异常而结束后,线程进入 TERMINATED。终止的线程不能再次调用 start()。
八、BLOCKED、WAITING 与 TIMED_WAITING 的区别
这三个状态都表现为“线程暂时不继续执行”,但原因不同:
| 状态 | 等待什么 | 常见触发方式 | 是否有超时 |
|---|---|---|---|
BLOCKED | 等待进入 synchronized 所需的监视器锁 | 竞争内置锁失败 | 没有主动超时参数 |
WAITING | 等待其他线程执行特定动作 | wait()、join() | 无期限 |
TIMED_WAITING | 等待动作或时间到达 | sleep(n)、wait(n)、join(n) | 有期限 |
另一个关键区别是:sleep() 不释放已持有的锁,而 wait() 会释放调用对象的监视器锁。这个差异会在等待唤醒机制中继续展开。
九、观察线程状态
public class StateDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println("创建后:" + worker.getState());
worker.start();
Thread.sleep(100);
System.out.println("休眠时:" + worker.getState());
worker.join();
System.out.println("结束后:" + worker.getState());
}
}
常见输出是 NEW、TIMED_WAITING、TERMINATED。不过多线程调度具有不确定性,观察状态本身也存在时间差,诊断复杂问题时应结合线程转储和日志,而不是只读取一次 getState()。
十、本篇小结
线程名称能够提高日志可读性;sleep() 让当前线程限时等待但不会释放锁;线程优先级和 yield() 都只影响调度倾向,不能保证业务顺序;守护线程适合后台辅助任务,不适合必须可靠收尾的工作;join() 则能明确表达“等待目标线程结束”。
Java 官方定义了 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 和 TERMINATED 六种状态。下一篇将进入多线程最关键的部分:共享数据为什么会出错,以及如何使用同步代码块、同步方法和 Lock 保证线程安全。