Thread 常用 API 与线程执行特征 | JavaSE
Thread 常用 API 与线程执行特征
一、学习目标
完成本章后,你应该能够:
- 熟练使用
getName()、setName()与currentThread()获取和设置线程信息。 - 理解
Thread.sleep()的真实含义,知道究竟是哪一个线程进入休眠。 - 理解
join()的真实语义,能够让当前线程等待指定线程执行结束。 - 理解线程执行顺序具有不确定性,不能通过一次运行结果推断固定调度规律。
- 了解线程优先级、守护线程、
yield()、interrupt()等常见控制能力。 - 理解线程启动、运行、等待与结束之间的大致状态变化。
- 能够通过线程名称和简单 API 调试多线程程序。
二、核心知识
2.1 Thread 不只是用来创建线程
前面已经学习:
Thread thread = new Thread(task);
thread.start();
但是 Thread 不只是:
“创建并启动线程的类”。
它还负责描述和控制线程的一系列属性与行为,例如:
线程叫什么名字?
当前代码是谁在执行?
线程是否还活着?
线程能不能暂停一段时间?
当前线程能不能等待另一个线程结束?
线程是不是守护线程?
线程有没有收到中断请求?
因此进入真正的多线程开发之后,需要掌握一组常用 Thread API。
2.2 本阶段常用 Thread API
| API | 作用 |
| ---------------------- | ---------------------------------- |
| getName() | 获取线程名称 |
| setName(String name) | 设置线程名称 |
| currentThread() | 获取当前正在执行代码的线程 |
| sleep(long millis) | 让当前线程休眠指定时间 |
| join() | 等待指定线程终止 |
| isAlive() | 判断线程是否还处于活动状态 |
| getPriority() | 获取线程优先级 |
| setPriority(int) | 设置平台线程优先级 |
| setDaemon(boolean) | 设置平台线程是否为守护线程 |
| isDaemon() | 判断线程是否为守护线程 |
| interrupt() | 向线程发送中断请求 |
| isInterrupted() | 查询线程是否已收到中断 |
| yield() | 向调度器提示当前线程愿意让出处理器 |
其中最需要熟练掌握的是:
线程名称
currentThread()
sleep()
join()
其他 API 先建立正确认识,不建议现阶段依赖它们设计复杂业务。
三、使用方法
3.1 getName():获取线程名称
每个 Thread 对象都有名称。
例如:
public class ThreadNameDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("任务执行");
});
System.out.println(t1.getName());
t1.start();
}
}
如果没有主动设置名称,传统平台线程通常会获得类似:
Thread-0
Thread-1
Thread-2
这样的自动名称。
线程名称本身不会改变程序执行逻辑,但是对于:
日志
Debug
异常分析
并发问题排查
非常重要。
如果输出只有:
开始执行
执行完成
出现问题时很难知道:
到底是哪条线程执行的?
如果输出:
订单线程-1:开始执行
订单线程-2:开始执行
定位问题就容易很多。
3.2 setName():设置线程名称
可以:
Thread t1 = new Thread(() -> {
System.out.println("执行任务");
});
t1.setName("下载线程");
t1.start();
然后:
System.out.println(t1.getName());
得到:
下载线程
更常见的做法是创建 Thread 时直接指定名称:
Thread t1 = new Thread(
() -> System.out.println("下载"),
"下载线程"
);
t1.start();
如果继承 Thread:
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(getName());
}
}
创建:
MyThread t1 = new MyThread("1号线程");
MyThread t2 = new MyThread("2号线程");
t1.start();
t2.start();
3.3 currentThread():到底是谁正在执行这段代码
非常重要:
Thread.currentThread()
返回:
当前正在执行这句代码的 Thread 对象。
例如:
public class CurrentThreadDemo {
public static void main(String[] args) {
Thread current =
Thread.currentThread();
System.out.println(
current.getName()
);
}
}
在普通 Java 程序中通常输出:
main
因为:
main()
正在由 main 线程执行。
3.4 currentThread() 在子线程中的效果
public class CurrentThreadDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
Thread current =
Thread.currentThread();
System.out.println(
current.getName()
);
}, "文件下载线程");
t1.start();
}
}
这里:
Thread.currentThread()
得到的是:
文件下载线程
而不是 main。
所以一定记住:
谁执行
currentThread(),得到的就是谁。
3.5 currentThread() 的典型应用:日志
例如:
class DownloadTask implements Runnable {
@Override
public void run() {
String name =
Thread.currentThread()
.getName();
System.out.println(
name + ":开始下载"
);
System.out.println(
name + ":下载完成"
);
}
}
启动:
new Thread(
new DownloadTask(),
"下载线程-A"
).start();
new Thread(
new DownloadTask(),
"下载线程-B"
).start();
日志可能:
下载线程-A:开始下载
下载线程-B:开始下载
下载线程-A:下载完成
下载线程-B:下载完成
这比单纯输出:
开始下载
下载完成
有价值得多。
3.6 sleep():让当前线程休眠
Thread 提供:
Thread.sleep(long millis)
例如:
Thread.sleep(1000);
表示:
让当前正在执行这句代码的线程暂时停止执行大约 1000 毫秒。
也就是:
1000 ms = 1 s
例如:
public class SleepDemo {
public static void main(String[] args)
throws InterruptedException {
for (int i = 1; i <= 5; i++) {
System.out.println(i);
Thread.sleep(1000);
}
}
}
效果大致为:
1
等待约1秒
2
等待约1秒
3
...
3.7 sleep() 到底让谁睡?
这是必考问题。
看:
t1.sleep(1000);
虽然通过 t1 写出来在语法上可以编译,但 sleep() 是:
static
静态方法。
真正的语义并不是:
让 t1 睡觉。
而是:
让当前执行
sleep()的线程休眠。
因此工程代码应该直接写:
Thread.sleep(1000);
而不是:
t1.sleep(1000);
避免产生错误理解。
例如:
public class SleepDemo {
public static void main(String[] args)
throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("子线程");
});
t1.start();
Thread.sleep(3000);
System.out.println("main继续");
}
}
休眠的是:
main线程
因为:
Thread.sleep(3000)
是 main 线程执行的。
3.8 sleep() 为什么需要处理 InterruptedException
方法:
Thread.sleep(...)
可能抛出:
InterruptedException
因此:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
或者在简单教学程序中:
public static void main(String[] args)
throws InterruptedException {
}
都可以。
InterruptedException 的意义是:
当前正在休眠、等待等状态中的线程收到了中断请求。
中断并不是简单粗暴地“杀死线程”。
后面会进一步解释。
3.9 join():等待指定线程结束
假设:
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("t1:" + i);
}
});
t1.start();
main 线程希望:
必须等 t1 全部执行完毕以后,我才能继续。
可以:
t1.join();
完整示例:
public class JoinDemo {
public static void main(String[] args)
throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("t1:" + i);
}
}, "线程A");
t1.start();
t1.join();
System.out.println(
"main:线程A已经结束"
);
}
}
执行模型:
main线程
t1.start()
│
├──────────────→ t1执行
│ │
t1.join() │
│ │
│ main等待 │
│ │
│ t1执行结束
│ │
←───────────────────┘
│
main继续
所以:
t1.join()的准确含义是:当前线程等待t1终止。
如果是 main 调用:
t1.join();
那么:
main等待t1
如果是 t2 调用:
t1.join();
则:
t2等待t1
3.10 join() 不是“提高线程优先级”
初学时经常把 join 称为:
线程插队
这个比喻可以帮助记忆现象,但并不准确。
例如:
t1.start();
t1.join();
不是:
把 t1 的优先级提升到最高
也不是:
强迫 CPU 只运行 t1
真正发生的是:
当前线程等待 t1 结束
系统中的其他线程仍然可以正常被调度执行。
3.11 join(long millis):最多等待一段时间
还可以:
t1.join(3000);
表示:
当前线程最多等待 t1 约 3000ms。
两种情况都可能结束等待:
情况一:
t1提前结束
→ join提前返回
情况二:
等待时间达到3000ms
→ join返回
所以它不是保证:
t1一定执行完成
而只是:
最多等待这么久
3.12 isAlive():线程是否还活着
可以:
thread.isAlive()
判断线程是否处于活动状态。
例如:
Thread t1 = new Thread(() -> {
System.out.println("执行任务");
});
System.out.println(t1.isAlive());
t1.start();
System.out.println(t1.isAlive());
由于线程调度具有不确定性:
start()
之后第二次检查时:
t1可能仍然活着
也可能任务已经执行完毕
所以不能通过这种代码假设固定结果。
这恰好再次说明:
多线程程序不要通过“我觉得它应该运行到这里了”判断线程状态。
四、原理与进阶
4.1 多线程为什么每次输出顺序可能不同
例如:
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("A:" + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("B:" + i);
}
});
t1.start();
t2.start();
一次可能:
A:0
B:0
A:1
B:1
...
另一次可能:
B:0
B:1
B:2
A:0
...
因为:
start()
只是让线程具备开始执行的条件。
具体什么时候获得执行机会由运行时和操作系统调度决定。
因此:
线程调度具有不确定性。
4.2 sleep() 不保证精确时间
例如:
Thread.sleep(1000);
不能理解为:
精确到纳秒地暂停 1000ms,然后第 1000ms 的那个瞬间立即运行。
更准确地说:
线程至少暂时停止执行
等待指定时间到达后
重新具备继续运行的机会
之后什么时候真正再次得到 CPU,还与调度有关。
所以 sleep():
不是高精度计时器
4.3 sleep() 与 join() 的区别
这是非常重要的对比。
sleep
Thread.sleep(1000);
关注的是:
时间
意思:
当前线程暂停一段时间。
join
t1.join();
关注的是:
另一个线程是否结束
意思:
当前线程等待 t1 结束。
所以:
sleep → 等时间
join → 等线程
4.4 start() 与 join() 还存在重要的线程间顺序保证
在 Java 内存模型中:
调用 t.start()
发生在:
t 中的所有动作之前
而线程 t 中完成的动作,又发生在:
其他线程成功从 t.join() 返回之前
这意味着 start() 和 join() 不只是控制执行流程,它们也参与线程之间的可见性与顺序保证。
目前先记住:
start
↓
建立“启动前 → 新线程”的关系
线程执行
↓
join成功返回
↓
建立“线程完成 → 等待者继续”的关系
Java 内存模型会在并发进阶阶段进一步理解。
4.5 线程优先级
平台线程存在优先级:
Thread.MIN_PRIORITY // 1
Thread.NORM_PRIORITY // 5
Thread.MAX_PRIORITY // 10
例如:
Thread t1 = new Thread(task);
t1.setPriority(
Thread.MAX_PRIORITY
);
获取:
int priority =
t1.getPriority();
但是必须特别注意:
线程优先级不能用来保证执行顺序。
不能写出这样的业务设计:
优先级10
所以一定先执行
优先级1
所以一定后执行
这是错误的。
线程调度最终由运行环境决定。
因此业务正确性绝不能依赖:
setPriority(...)
4.6 JDK 21 中虚拟线程的优先级
JDK 21 中:
虚拟线程
的优先级固定为:
Thread.NORM_PRIORITY
即:
5
对虚拟线程调用:
setPriority(...)
不会像平台线程那样改变其优先级。
这进一步说明:
优先级不是现代 Java 并发编程中应该依赖的核心控制机制。
4.7 守护线程
平台线程可以:
thread.setDaemon(true);
将线程设置成:
守护线程(Daemon Thread)
普通线程通常称为:
非守护线程
例如:
Thread daemon = new Thread(() -> {
while (true) {
System.out.println("后台服务");
}
});
daemon.setDaemon(true);
daemon.start();
需要注意:
setDaemon(true)
必须在:
start()
之前完成。
不能:
thread.start();
thread.setDaemon(true);
4.8 守护线程真正意味着什么
当 JVM 中所有已经启动的:
非守护线程
全部终止后:
JVM 的关闭流程可以开始
守护线程本身不会为了“自己还没执行完”而阻止 JVM 关闭。
所以不要设计:
必须保证完成的重要保存任务
完全依赖守护线程执行结束。
例如:
最后一次转账必须落盘
就不能只想着:
“反正守护线程会慢慢保存完。”
因为 JVM 进入关闭流程时,不保证守护线程能把剩余业务完整做完。
4.9 yield():礼让只是提示
方法:
Thread.yield();
表示:
当前线程向调度器提示:我愿意暂时让出当前处理器使用机会。
但是:
调度器可以接受
也可以忽略
所以:
yield()
不能用于:
保证线程顺序
保证公平性
实现可靠同步
它通常不适合普通业务代码。
4.10 interrupt():中断不是强制杀死
可以:
thread.interrupt();
它表达的是:
向目标线程发送中断请求。
不是:
立即强制终止线程
例如线程正在:
Thread.sleep(10000);
其他线程:
thread.interrupt();
那么休眠线程可能收到:
InterruptedException
所以线程中断的核心思想更接近:
协作式取消
即:
A:
“请停止/请注意中断”
B:
收到信号后决定如何正确退出
而不是直接把线程从 CPU 上“击毙”。
4.11 不要使用 stop()、suspend()、resume() 作为现代方案
Thread 中历史上存在:
stop()
suspend()
resume()
这些 API 已被废弃,不应该作为现代 Java 并发程序的控制方式。
尤其:
stop()
可能破坏对象状态的一致性。
而:
suspend()
可能导致线程持有锁时被暂停,从而产生严重问题。
现代程序应采用:
中断机制
同步机制
并发工具类
任务取消机制
而不是强制暂停或杀死线程。
五、实践应用
5.1 下载任务的线程日志
public class DownloadDemo {
public static void main(String[] args) {
Runnable task = () -> {
String name =
Thread.currentThread()
.getName();
System.out.println(
name + ":开始下载"
);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread()
.interrupt();
return;
}
System.out.println(
name + ":下载完成"
);
};
Thread t1 =
new Thread(
task,
"图片下载线程"
);
Thread t2 =
new Thread(
task,
"视频下载线程"
);
t1.start();
t2.start();
}
}
5.2 主线程等待初始化任务完成
假设:
后台线程负责加载配置
main 后续业务必须依赖配置
就可以:
public class InitDemo {
public static void main(String[] args)
throws InterruptedException {
Thread initThread =
new Thread(() -> {
System.out.println(
"正在加载配置..."
);
}, "配置加载线程");
initThread.start();
initThread.join();
System.out.println(
"配置加载完成,启动系统"
);
}
}
这里 join() 的业务含义非常明确:
没有配置
↓
后面的程序不能继续
↓
等待配置线程结束
六、常见问题
6.1 Thread.sleep() 是让指定 Thread 对象睡觉吗?
不是。
sleep() 是静态方法。
谁执行:
Thread.sleep(...)
谁进入休眠。
6.2 sleep() 到时间后一定立即继续吗?
不一定。
休眠时间结束后,线程重新具备继续运行的条件,但还要等待调度。
6.3 join() 是线程插队吗?
只能把“插队”当作入门比喻。
准确语义:
当前线程等待目标线程终止。
6.4 t1.join() 是谁在等待?
不是 t1 等。
而是:
谁调用
t1.join(),谁等待 t1。
6.5 设置最高优先级后一定最先执行吗?
不保证。
绝对不要使用线程优先级保证业务执行顺序。
6.6 setDaemon(true) 什么时候调用?
必须在:
start()
之前。
6.7 interrupt() 会立即终止线程吗?
不会。
它本质上属于:
中断请求
线程需要正确响应中断。
6.8 可以根据一次运行结果判断调度规律吗?
不可以。
例如一次结果是:
A
B
A
B
不能说明以后一定交替执行。
七、练习与验收
7.1 知识问答
getName()和setName()分别做什么?currentThread()返回谁?- main 方法中调用
currentThread()通常得到哪个线程? Thread.sleep(1000)让谁休眠?- 为什么不推荐写成
t1.sleep(1000)? sleep()到时间以后是否一定立即执行?t1.join()的准确含义是什么?- 为什么把
join()简单理解成“插队”不够准确? sleep()与join()的核心区别是什么?- 为什么线程输出顺序不固定?
setPriority()能否保证线程先执行?- 平台线程优先级的大致范围是什么?
- 守护线程有什么执行特征?
- 为什么不能让重要业务仅依赖守护线程最终完成?
yield()能否保证把 CPU 让给另一个指定线程?interrupt()是不是强制杀死线程?
7.2 代码阅读
不要运行:
public class ThreadRead {
public static void main(String[] args)
throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(
Thread.currentThread()
.getName()
);
}, "A");
t1.start();
t1.join();
System.out.println(
Thread.currentThread()
.getName()
);
}
}
回答:
- 子线程输出什么名称?
- 最后一行通常输出什么?
- 最后一行能否早于线程 A 的输出?
- 为什么?
阅读:
Thread t1 = new Thread(() -> {
System.out.println("A");
});
t1.start();
Thread.sleep(3000);
System.out.println("B");
回答:
- 是 t1 睡 3 秒还是 main 睡 3 秒?
"A"是否必须等 3 秒后才能输出?"B"为什么需要等待?
7.3 手写代码
任务一:
创建:
下载线程
日志线程
要求两个线程分别输出自己的线程名称。
任务二:
使用:
Thread.sleep(1000);
每隔大约一秒输出一个数字,共输出 5 个。
任务三:
启动线程 A,让 A 输出 1~10。
main 调用:
a.join();
确保:
main结束
只在线程 A 终止之后输出。
7.4 Debug
代码:
Thread t = new Thread(task);
t.start();
t.setDaemon(true);
判断问题并修复。
代码:
Thread t1 = new Thread(task);
t1.setPriority(10);
t1.start();
System.out.println(
"因为优先级最高,所以t1一定执行完了"
);
指出逻辑错误。
代码:
Thread t1 = new Thread(task);
t1.start();
t1.join();
System.out.println("main继续");
有人解释:
join()把 t1 的优先级提高,所以 t1 一定抢占 CPU。
判断这个解释是否正确,并给出准确解释。
7.5 综合训练
编写“系统初始化模拟程序”。
系统需要完成:
线程 A:加载用户配置
线程 B:加载缓存数据
main:启动业务系统
要求:
- A、B 分别具有明确线程名称。
- A、B 使用
sleep()模拟耗时。 - A、B 同时启动。
- main 必须等待 A、B 都执行结束。
- 最终才输出:
系统启动完成
思考:
为什么这里需要 join,
而不是简单 sleep(5000)?
7.6 本章验收
不查看资料能够完成:
getName
setName
currentThread
sleep
join
五个 API 的口述与手写。
并且能够解释:
sleep 是等时间
join 是等线程
priority 不保证顺序
yield 不保证礼让
interrupt 不等于强杀
daemon 不阻止 JVM 进入关闭流程
即可进入下一章:
线程安全问题