2025/12/29 14:13:31
网站建设
项目流程
针对网站开发者的问答网站,商丘网站制作报价信赖赛憬科技,建设工程申报系统网站,深圳展厅设计企业展厅各线程控制方法的典型使用场景#xff08;深度详解#xff09;
针对初学者的理解特点#xff0c;我会对join()、sleep()、wait()/notify()、yield()、LockSupport.park()/unpark()这 5 个核心方法#xff0c;按照适配场景、通用做法、实战案例、避坑指南、小总结的维度逐一…各线程控制方法的典型使用场景深度详解针对初学者的理解特点我会对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计算完成sum5050 当前线程主线程拿到结果sum5050案例 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、SemaphoreJUC 工具类底层都用它解决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)的线程被唤醒的目标线程workThreadunpark()的参数。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 检查条件锁对象必须唯一不依赖非精准的定时 / 调度。通过上面的案例和明确的线程角色标注你可以直接复制代码运行结合实际效果理解每个方法的使用场景这比死记硬背更有效。