各线程控制方法的典型使用场景(深度详解)
针对初学者的理解特点,我会对join()、sleep()、wait()/notify()、yield()、locksupport.park()/unpark()这 5 个核心方法,按照适配场景、通用做法、实战案例、避坑指南、小总结的维度逐一拆解,全程明确标注「当前线程」和「目标线程」,所有案例都是可直接运行的极简代码,保证一看就懂、一跑就通。
一、thread.join ():等其他线程 “干完活” 再继续
1. 适配场景(什么时候用?)
join()的核心是当前线程依赖其他线程(目标线程)的执行结果,常见场景:
- 主线程(当前线程)需要子线程(目标线程)的计算结果、下载的文件、查询的数据库数据等;
- 多线程任务汇总(主线程作为当前线程,等待多个子线程(目标线程)处理完任务后统一汇总);
- 控制线程执行顺序(线程 a(当前线程)等线程 b(目标线程)执行完后,再执行自身逻辑)。
2. 通用做法(标准写法)
- 先创建并启动目标线程(被等待的线程);
- 在当前线程中调用目标线程的
join()方法; - 必须处理
interruptedexception(捕获或声明抛出); - 多线程场景下,当前线程逐个调用目标线程的
join()或用循环批量调用。
3. 实战案例
案例 1:主线程依赖子线程的计算结果
明确线程角色:
- 当前线程:main 主线程(执行
calcthread.join()的线程); - 目标线程:calcthread 子线程(被主线程等待的线程)。
public class joindemo1 {
// 共享变量存储子线程的计算结果
private static int sum = 0;
public static void main(string[] args) throws interruptedexception {
// 目标线程:计算1-100的和
thread calcthread = new thread(() -> {
for (int i = 1; i <= 100; i++) {
sum += i;
}
system.out.println("目标线程(calcthread):计算完成,sum=" + sum);
});
// 启动目标线程
calcthread.start();
// 当前线程(主线程)等待目标线程计算完成
calcthread.join();
// 当前线程(主线程)使用目标线程的结果
system.out.println("当前线程(主线程):拿到结果,sum=" + sum);
}
}
输出结果:
目标线程(calcthread):计算完成,sum=5050
当前线程(主线程):拿到结果,sum=5050
案例 2:多线程任务汇总(3 个线程下载文件,主线程等全部完成)
明确线程角色:
- 当前线程:main 主线程(执行
t1.join()/t2.join()/t3.join()的线程); - 目标线程:t1、t2、t3 下载线程(被主线程等待的线程)。
public class joindemo2 {
public static void main(string[] args) throws interruptedexception {
// 创建3个目标线程(下载线程)
thread t1 = new thread(() -> system.out.println("目标线程(t1):文件1下载完成"), "下载线程1");
thread t2 = new thread(() -> system.out.println("目标线程(t2):文件2下载完成"), "下载线程2");
thread t3 = new thread(() -> system.out.println("目标线程(t3):文件3下载完成"), "下载线程3");
// 启动所有目标线程
t1.start();
t2.start();
t3.start();
// 当前线程(主线程)等待所有目标线程完成
t1.join();
t2.join();
t3.join();
// 当前线程(主线程)汇总结果
system.out.println("当前线程(主线程):所有文件下载完成,开始合并文件!");
}
}
输出结果:
目标线程(t1):文件1下载完成
目标线程(t2):文件2下载完成
目标线程(t3):文件3下载完成
当前线程(主线程):所有文件下载完成,开始合并文件!
4. 避坑指南(初学者必看)
| 坑点 | 表现 | 解决方案 |
|---|---|---|
忘记处理interruptedexception | 编译报错 | 要么用try-catch捕获,要么在方法上声明throws interruptedexception |
目标线程自身调用join()(比如 t 中调用t.join()) | 线程永远阻塞(自己等自己完成) | 绝对避免这种写法 |
多实例的join()导致 “伪等待” | 想让线程全局互斥,却用了不同实例的join() | 保证所有线程使用同一个实例,或用静态方法 + 类锁 |
依赖join(long millis)的精准超时 | 实际唤醒时间比指定时间长 | 仅把超时作为 “最大等待时间”,不依赖其做精准定时 |
5. 小总结
join()是 **“等待依赖” 的工具 **,核心记住:
- 当前线程:调用
join()的线程(主动发起等待的线程); - 目标线程:被调用
join()的线程(被等待的线程); - 谁调用
join(),谁就等;等的是目标线程完成,不是自己暂停。
二、thread.sleep ():让当前线程 “歇一会儿”
1. 适配场景(什么时候用?)
sleep()的核心是当前线程主动暂停指定时间,时间到自动唤醒,无目标线程(只是当前线程自身行为),常见场景:
- 模拟延迟(比如验证码倒计时、测试时模拟网络延迟);
- 降低 cpu 占用(避免空循环导致 cpu 100% 占用);
- 简单定时(比如每隔 1 秒打印一次日志,非精准场景)。
2. 通用做法(标准写法)
- 当前线程调用
thread.sleep(),用try-catch包裹(必须处理interruptedexception); - 指定合理的暂停时间(毫秒为单位);
- 不要依赖
sleep()做精准定时(系统调度会有误差); - 同步块中使用
sleep()时,明确其不释放锁的特性。
3. 实战案例
案例 1:验证码倒计时(模拟 60 秒倒计时)
明确线程角色:
- 当前线程:main 主线程(执行
thread.sleep(1000)的线程); - 无目标线程(只是主线程自身暂停)。
public class sleepdemo1 {
public static void main(string[] args) {
// 验证码倒计时60秒
int count = 60;
while (count > 0) {
system.out.println("当前线程(主线程):验证码倒计时:" + count + "秒");
try {
// 当前线程(主线程)暂停1秒
thread.sleep(1000);
} catch (interruptedexception e) {
e.printstacktrace();
}
count--;
}
system.out.println("当前线程(主线程):验证码已过期,请重新获取!");
}
}
输出结果:每秒打印一次倒计时,直到 60 秒结束。
案例 2:降低 cpu 占用(空循环加 sleep)
明确线程角色:
- 当前线程:monitorthread 监控线程(执行
thread.sleep(5000)的线程); - 无目标线程。
public class sleepdemo2 {
public static void main(string[] args) {
// 模拟一个持续运行的监控线程(当前线程)
thread monitorthread = new thread(() -> {
while (true) {
// 执行监控逻辑
system.out.println("当前线程(monitorthread):监控系统运行中...");
try {
// 当前线程(monitorthread)每隔5秒监控一次
thread.sleep(5000);
} catch (interruptedexception e) {
e.printstacktrace();
// 被中断时退出循环
break;
}
}
});
monitorthread.start();
}
}
效果:线程每隔 5 秒执行一次,cpu 占用率几乎为 0(如果不加 sleep,空循环会让 cpu 核心占满)。
4. 避坑指南(初学者必看)
| 坑点 | 表现 | 解决方案 |
|---|---|---|
认为sleep()会释放锁 | 同步块中调用sleep(),其他线程无法获取锁 | 记住:sleep()不释放任何锁,要释放锁用wait() |
用sleep()做精准定时 | 实际执行时间比预期长 | 精准定时用scheduledexecutorservice,而非sleep() |
忽略sleep()的中断异常 | 线程被中断后,无法正常退出 | 在catch块中处理中断(比如退出循环) |
| 睡眠时间设为 0 | 等同于thread.yield()(主动让步),但不推荐 | 要让步直接用yield(),不要用sleep(0) |
5. 小总结
sleep()是 **“自我暂停” 的工具 **,核心记住:
- 当前线程:执行
sleep()的线程(自身暂停的线程); - 无目标线程(只是自己歇,和其他线程无关);
- 自己歇,不释放锁,时间到必醒;适合简单延迟和降 cpu,不适合精准定时和线程协作。
三、object.wait ()/notify ()/notifyall ():线程间的 “对话工具”
1. 适配场景(什么时候用?)
wait()/notify()的核心是线程间的条件协作,涉及两类线程:
- 等待线程(当前线程):调用
wait()的线程(因条件不满足而等待的线程); - 唤醒线程(目标线程):调用
notify()/notifyall()的线程(满足条件后唤醒等待线程的线程)。
常见场景:
- 生产者消费者模式(生产者是唤醒线程,消费者是等待线程;反之亦然);
- 线程间条件等待(线程 a 是等待线程,线程 b 是唤醒线程);
- 任务队列的消费(消费线程是等待线程,添加任务的线程是唤醒线程)。
2. 通用做法(标准写法,记死这个模板!)
- 必须在同步块 / 同步方法中调用(持有对象的锁);
- 用
while循环检查条件(防止虚假唤醒); - 等待线程(当前线程):
synchronized(锁对象) { while(条件不满足) { 锁对象.wait(); } // 执行操作 }; - 唤醒线程(目标线程):
synchronized(锁对象) { // 改变条件 锁对象.notifyall(); }; - 优先用
notifyall()而非notify()(避免唤醒错线程)。
3. 实战案例
案例 1:经典生产者消费者模式(队列满则生产者等,队列空则消费者等)
明确线程角色:
- 等待线程(当前线程):队列满时的生产者线程、队列空时的消费者线程;
- 唤醒线程(目标线程):消费后的消费者线程(唤醒生产者)、生产后的生产者线程(唤醒消费者)。
public class waitnotifydemo {
// 共享队列(用数组模拟,容量为3)
private static final int[] queue = new int[3];
// 队列当前元素个数
private static int count = 0;
// 锁对象(所有线程共用同一个锁)
private static final object lock = new object();
// 生产者线程:生产产品(往队列加元素)
static class producer extends thread {
@override
public void run() {
while (true) {
synchronized (lock) {
// 队列满,当前线程(生产者)成为等待线程,等待消费者唤醒
while (count == queue.length) {
try {
system.out.println("当前线程(生产者):队列满,等待消费者消费...");
lock.wait();
} catch (interruptedexception e) {
e.printstacktrace();
}
}
// 生产产品
queue[count] = (int) (math.random() * 100);
system.out.println("当前线程(生产者):生产" + queue[count]);
count++;
// 当前线程(生产者)成为唤醒线程,唤醒等待的消费者
lock.notifyall();
}
// 模拟生产间隔
try {
thread.sleep(500);
} catch (interruptedexception e) {
e.printstacktrace();
}
}
}
}
// 消费者线程:消费产品(从队列取元素)
static class consumer extends thread {
@override
public void run() {
while (true) {
synchronized (lock) {
// 队列空,当前线程(消费者)成为等待线程,等待生产者唤醒
while (count == 0) {
try {
system.out.println("当前线程(消费者):队列空,等待生产者生产...");
lock.wait();
} catch (interruptedexception e) {
e.printstacktrace();
}
}
// 消费产品
count--;
system.out.println("当前线程(消费者):消费" + queue[count]);
// 当前线程(消费者)成为唤醒线程,唤醒等待的生产者
lock.notifyall();
}
// 模拟消费间隔
try {
thread.sleep(800);
} catch (interruptedexception e) {
e.printstacktrace();
}
}
}
}
public static void main(string[] args) {
// 启动1个生产者、1个消费者
new producer().start();
new consumer().start();
}
}
输出结果:
当前线程(生产者):生产88
当前线程(生产者):生产12
当前线程(生产者):生产56
当前线程(生产者):队列满,等待消费者消费...
当前线程(消费者):消费56
当前线程(生产者):生产34
当前线程(消费者):消费34
当前线程(消费者):消费12
当前线程(消费者):消费88
当前线程(消费者):队列空,等待生产者生产...
当前线程(生产者):生产77
...
案例 2:线程等待初始化完成(子线程等主线程初始化完毕后执行)
明确线程角色:
- 等待线程(当前线程):workthread 子线程(执行
lock.wait()的线程); - 唤醒线程(目标线程):main 主线程(执行
lock.notifyall()的线程)。
public class waitnotifydemo2 {
// 初始化完成标记
private static boolean initdone = false;
// 锁对象
private static final object lock = new object();
public static void main(string[] args) {
// 等待线程(当前线程):workthread子线程,等待初始化完成
thread workthread = new thread(() -> {
synchronized (lock) {
// 等待初始化完成
while (!initdone) {
try {
system.out.println("当前线程(workthread):等待初始化...");
lock.wait();
} catch (interruptedexception e) {
e.printstacktrace();
}
}
system.out.println("当前线程(workthread):初始化完成,开始工作!");
}
});
workthread.start();
// 唤醒线程(目标线程):主线程,执行初始化并唤醒子线程
try {
thread.sleep(2000); // 模拟初始化耗时
synchronized (lock) {
initdone = true;
system.out.println("当前线程(主线程):初始化完成,唤醒子线程!");
lock.notifyall();
}
} catch (interruptedexception e) {
e.printstacktrace();
}
}
}
输出结果:
当前线程(workthread):等待初始化...
当前线程(主线程):初始化完成,唤醒子线程!
当前线程(workthread):初始化完成,开始工作!
4. 避坑指南(初学者必看,这部分最容易错!)
| 坑点 | 表现 | 解决方案 |
|---|---|---|
不在同步块中调用wait()/notify() | 抛出illegalmonitorstateexception | 必须先获取锁,再调用方法 |
用if代替while检查条件 | 出现虚假唤醒(线程被唤醒后,条件依然不满足) | 强制用while循环检查条件 |
用notify()代替notifyall() | 只唤醒一个线程,若该线程不满足条件,其他线程永远等待 | 优先用notifyall(),除非明确只有一个等待线程 |
| 唤醒后忘记修改条件 | 线程被唤醒后,条件依然不满足,再次等待 | 唤醒前必须修改条件(比如initdone = true) |
| 锁对象不唯一 | 不同线程用不同的锁对象,无法通信 | 所有线程共用同一个锁对象 |
5. 小总结
wait()/notify()是 **“线程对话” 的工具 **,核心记住:
- 等待线程(当前线程):调用
wait()的线程(因条件不满足等待的线程); - 唤醒线程(目标线程):调用
notifyall()的线程(满足条件后唤醒的线程); - 在同步块中用 while 检查条件,用 notifyall () 唤醒;是线程协作的基础,生产者消费者模式的核心实现方式。
四、thread.yield ():让当前线程 “让个座”
1. 适配场景(什么时候用?)
yield()的核心是当前线程主动放弃 cpu 执行权,回到就绪态,让操作系统重新调度,无目标线程(只是当前线程的让步行为),常见场景:
- 非核心任务让步(比如后台统计、日志收集等低优先级任务,主动让 cpu 给高优先级的业务线程);
- 提升并发公平性(防止单个线程长期占用 cpu,导致其他线程饥饿);
- 测试场景(模拟线程调度的随机性)。
2. 通用做法(标准写法)
- 当前线程直接调用
thread.yield()(无需处理异常); - 不依赖
yield()保证执行顺序(操作系统不一定采纳让步请求); - 仅在非核心任务中使用,不用于核心业务逻辑。
3. 实战案例
案例:高优先级业务线程与低优先级统计线程(统计线程主动让步)
明确线程角色:
- 当前线程:statthread 统计线程(执行
thread.yield()的线程); - 无目标线程(只是主动给其他线程让 cpu)。
public class yielddemo {
// 业务线程(高优先级,处理核心逻辑)
static class businessthread extends thread {
public businessthread() {
// 设置高优先级(1-10,默认5)
setpriority(thread.max_priority);
}
@override
public void run() {
for (int i = 1; i <= 10; i++) {
system.out.println("业务线程:处理订单" + i);
}
}
}
// 统计线程(低优先级,后台统计):当前线程,执行yield()让步
static class statthread extends thread {
public statthread() {
// 设置低优先级
setpriority(thread.min_priority);
}
@override
public void run() {
for (int i = 1; i <= 10; i++) {
// 当前线程(statthread)主动让步,让业务线程先执行
thread.yield();
system.out.println("当前线程(statthread):统计订单" + i);
}
}
}
public static void main(string[] args) {
new businessthread().start();
new statthread().start();
}
}
输出结果:业务线程的订单处理会优先打印,统计线程的打印会穿插在其中(具体顺序由操作系统调度决定)。
4. 避坑指南(初学者必看)
| 坑点 | 表现 | 解决方案 |
|---|---|---|
依赖yield()保证执行顺序 | 线程执行顺序混乱(操作系统可能忽略让步请求) | 不要用yield()控制执行顺序,用join()或wait()/notify() |
认为yield()会让线程阻塞 | 线程只是回到就绪态,随时可能被重新调度 | 记住:yield()不阻塞,只是 “让个座” |
频繁调用yield() | 降低程序执行效率(频繁调度线程) | 仅在非核心任务中偶尔调用 |
5. 小总结
yield()是 **“主动让步” 的工具 **,核心记住:
- 当前线程:执行
yield()的线程(主动让 cpu 的线程); - 无目标线程(只是让 cpu,不针对特定线程);
- 让 cpu,不阻塞,操作系统不一定采纳;适合提升并发公平性,不适合控制执行顺序。
五、locksupport.park ()/unpark ():灵活的 “线程开关”
1. 适配场景(什么时候用?)
locksupport的park()/unpark()涉及两类线程:
- 阻塞线程(当前线程):调用
park()的线程(被阻塞的线程); - 唤醒线程(目标线程):调用
unpark(thread t)的线程(唤醒指定线程的线程),unpark()的参数就是被唤醒的目标线程。
常见场景:
- 自定义同步工具(比如实现自己的
countdownlatch、semaphore,juc 工具类底层都用它); - 解决
wait()/notify()的 “唤醒丢失” 问题(可先unpark再park); - 灵活的线程通信(精准唤醒指定线程)。
2. 通用做法(标准写法)
- 阻塞线程(当前线程):调用
locksupport.park()阻塞自身; - 唤醒线程(目标线程):调用
locksupport.unpark(thread t)唤醒指定的阻塞线程; - 检查中断状态:
park()被中断后不会抛异常,需用thread.interrupted()检查并处理。
3. 实战案例
案例 1:基本的 park/unpark(主线程唤醒子线程)
明确线程角色:
- 阻塞线程(当前线程):workthread 子线程(执行
locksupport.park()的线程); - 唤醒线程(目标线程):main 主线程(执行
locksupport.unpark(workthread)的线程); - 被唤醒的目标线程:workthread(
unpark()的参数)。
import java.util.concurrent.locks.locksupport;
public class locksupportdemo1 {
public static void main(string[] args) {
// 阻塞线程(当前线程):workthread子线程
thread workthread = new thread(() -> {
system.out.println("当前线程(workthread):开始执行,准备阻塞...");
// 当前线程(workthread)阻塞自身
locksupport.park();
system.out.println("当前线程(workthread):被唤醒,继续执行!");
}, "工作线程");
workthread.start();
// 唤醒线程(主线程):延迟2秒后唤醒目标线程(workthread)
try {
thread.sleep(2000);
system.out.println("当前线程(主线程):唤醒目标线程(workthread)!");
locksupport.unpark(workthread);
} catch (interruptedexception e) {
e.printstacktrace();
}
}
}
输出结果:
当前线程(workthread):开始执行,准备阻塞...
当前线程(主线程):唤醒目标线程(workthread)!
当前线程(workthread):被唤醒,继续执行!
案例 2:解决 “唤醒丢失” 问题(先 unpark 再 park)
明确线程角色:
- 阻塞线程(当前线程):workthread 子线程(执行
locksupport.park()的线程); - 唤醒线程(目标线程):main 主线程(执行
locksupport.unpark(workthread)的线程); - 被唤醒的目标线程:workthread。
import java.util.concurrent.locks.locksupport;
public class locksupportdemo2 {
public static void main(string[] args) {
// 阻塞线程(当前线程):workthread子线程
thread workthread = new thread(() -> {
// 先被unpark,再park(不会阻塞)
system.out.println("当前线程(workthread):准备阻塞...");
locksupport.park();
system.out.println("当前线程(workthread):执行完成!");
});
// 唤醒线程(主线程):先执行unpark,唤醒目标线程(workthread)
locksupport.unpark(workthread);
system.out.println("当前线程(主线程):先执行unpark(),唤醒目标线程(workthread)");
// 启动阻塞线程
workthread.start();
}
}
输出结果:
当前线程(主线程):先执行unpark(),唤醒目标线程(workthread)
当前线程(workthread):准备阻塞...
当前线程(workthread):执行完成!
(如果是wait()/notify(),先 notify 再 wait 会导致线程永远阻塞,而park()/unpark()不会)
4. 避坑指南(初学者必看)
| 坑点 | 表现 | 解决方案 |
|---|---|---|
忽略park()的中断状态 | 线程被中断后,park()返回但不抛异常,线程继续执行 | 用thread.interrupted()检查中断状态,处理中断逻辑 |
认为unpark()可以多次生效 | 多次unpark()等同于一次(许可只能用一次) | 每次park()前确保只有一次unpark() |
过度使用park()/unpark() | 简单场景下代码复杂度高 | 简单场景用wait()/notify(),复杂场景(自定义同步工具)再用park()/unpark() |
5. 小总结
locksupport是 **“底层线程控制工具”**,核心记住:
- 阻塞线程(当前线程):执行
park()的线程(被阻塞的线程); - 唤醒线程:执行
unpark()的线程; - 被唤醒的目标线程:
unpark()的参数指定的线程; - 不依赖对象锁,可先 unpark 再 park,精准唤醒指定线程;是 juc 工具的基础,初学者先掌握用法,后续学并发框架时会更易理解。
六、所有方法的核心对比表(含线程角色,初学者收藏)
| 方法 | 核心作用 | 当前线程 | 目标线程 | 释放锁? | 唤醒方式 |
|---|---|---|---|---|---|
join() | 等其他线程完成 | 调用join()的线程 | 被调用join()的线程 | 释放目标线程对象的锁 | 目标线程完成后自动唤醒 |
sleep() | 自我暂停指定时间 | 执行sleep()的线程 | 无 | 不释放 | 时间到自动唤醒 |
wait() | 条件不满足时等待 | 调用wait()的线程 | 调用notifyall()的线程 | 释放锁对象的锁 | 其他线程唤醒 |
notify() | 唤醒等待的线程 | 调用notify()的线程 | 被唤醒的等待线程 | 不释放(仍持有锁) | 主动调用notify()/notifyall() |
yield() | 主动让步 cpu | 执行yield()的线程 | 无 | 不释放 | 操作系统重新调度 |
park() | 阻塞自身 | 执行park()的线程 | 调用unpark()的线程 | 不释放 | 其他线程调用unpark()或中断 |
unpark() | 唤醒指定线程 | 执行unpark()的线程 | unpark()参数指定的线程 | 不释放 | 主动调用unpark() |
七、初学者终极总结
记准线程角色:
- 谁调用方法,谁大概率是当前线程;
- 方法参数或被 操作的线程,通常是目标线程;
- 无参数、无被 操作线程的方法(如
sleep()、yield()),一般无目标线程。
记准核心用途:
- 等别人结果用
join()(当前线程等目标线程); - 自己歇会儿用
sleep()(当前线程自我暂停,无目标线程); - 线程对话用
wait()/notify()(当前线程等待,目标线程唤醒); - 主动让步用
yield()(当前线程让 cpu,无目标线程); - 灵活控制用
park()/unpark()(当前线程阻塞,目标线程精准唤醒)。
- 等别人结果用
避坑核心点:
- 所有中断异常都要处理;
wait()必须用 while 检查条件;- 锁对象必须唯一;
- 不依赖非精准的定时 / 调度。
通过上面的案例和明确的线程角色标注,你可以直接复制代码运行,结合实际效果理解每个方法的使用场景,这比死记硬背更有效。
总结
到此这篇关于java线程方法之从线程角色到实战避坑的文章就介绍到这了,更多相关java线程角色到实战避坑内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论