多线程编程之——终止(打断)正在执行中的线程
多线程编程之——终止(打断)正在执行中的线程
- ps:文字有点多,想看结果的,直接跳转:《二》
一、基础知识
1、我们基于spring开发,把线程都交给spring
- 把线程交给spring管理好不好?
将线程交给Spring管理是一个常见的做法,特别是在基于Spring的应用程序中。通过将线程纳入Spring的管理范围,你可以利用Spring的依赖注入和生命周期管理功能,更好地控制线程的生命周期和资源。这样做也有助于避免手动管理线程带来的潜在问题,比如线程泄露和资源管理不当。但是需要注意的是,要确保在使用Spring管理线程时,遵循最佳实践,避免出现死锁和性能问题。
还有个好处:可以把spring的bean注入到线程中,然后直接使用。(如注入service完成查询数据)
如果把线程交给spring管理,注入线程的地方全是单例,如果要使用这个线程来完成两个不同的任务,需要把单例变成多例模式,spring多例模式见:SpringBoot实现多例模式
- 一个线程默认当做执行一个任务的最小单元
所以,当你需要执行一个任务的时候,应该把参数封装到线程中,然后在线程中去执行你的任务(方法)
2、启动Thread线程是调用start还是run?
在Java中,启动一个Thread线程应该调用start方法而不是run方法。调用start方法会使Thread进入就绪状态,并让系统调度器来调用run方法。直接调用run方法只会在当前线程中执行run方法的代码,而不会创建新的线程。因此,正确的做法是调用start方法来启动一个新的线程。
3、打断执行中的线程
- 如果我new了一个Thread线程,它一直在执行中(无限阻塞),我后面调用了interrupt打断线程,这个Thread线程会一直阻塞还是结束任务?
如果你在一个线程对象上调用interrupt方法,会设置线程的中断状态,并且如果线程正在阻塞中(比如在sleep、wait、join等方法中),它会抛出InterruptedException并清除中断状态。但如果线程正在执行普通的任务,它不会自动停止,而是需要在任务中检查中断状态并做出相应的处理。因此,你需要在任务中定期检查线程的中断状态,并在适当的时候结束任务。
- 我调用interrupt的线程,如果一直处于睡眠中,jvm会自动回收吗?
当你调用一个处于睡眠状态的线程的interrupt方法时,它会抛出InterruptedException并清除中断状态,但并不会导致线程被自动回收。处于睡眠状态的线程会继续睡眠直到其睡眠时间结束或者被唤醒。如果你希望中断一个处于睡眠状态的线程并结束它的任务,你需要在任务中捕获InterruptedException并做出相应的处理。
- 捕获InterruptedException并在捕获到异常时结束任务或者进行清理工作。示例:
public class MyTask implements Runnable { public void run() { try { // 在这里调用sleep方法使线程进入睡眠状态(模仿无限阻塞) Thread.sleep(100000); } catch (InterruptedException e) { // 捕获InterruptedException并做出相应的处理 // 可以结束任务或者进行清理工作 Thread.currentThread().interrupt(); // 重新设置中断状态 return; // 结束任务 } // 其他任务逻辑 } }
在上面的示例中,当线程处于睡眠状态时,如果外部调用了interrupt方法,线程会抛出InterruptedException,然后在catch块中重新设置中断状态并结束任务。
要想终止线程,必须在线程中调用:Thread.currentThread().interrupt();方法,而不是外部调用。
二、打断线程
1、打断new出来的线程
- 也可以把线程交给spring管理
- 直接new一个线程,然后在需要打断的地方进行打断,示例:
package com.cc.jschdemo.terminationThread; /** * <p>基本打断</p> * * @author cc * @since 2023/11/23 */ public class BasicInterrupt { /** * 终止 * @param args */ public static void main(String[] args) { //t1:模仿执行任务的线程 Thread t1 = new Thread(() -> { try { //步奏1 Thread.sleep(1000); System.out.println("完成:步奏1"); //步奏2(模仿阻塞) while (true) { Thread.sleep(1000); System.out.println("完成:步奏2"); } }catch (InterruptedException e) { e.printStackTrace(); //捕获异常:InterruptedException,然后打断 //☆☆打断,终止当前线程☆☆ Thread.currentThread().interrupt(); } }); //t2:模仿其他操作,打断线程 Thread t2 = new Thread(() -> { try { Thread.sleep(3100); System.out.println("打断线程!"); //打断t1线程 t1.interrupt(); }catch (Exception e) { e.printStackTrace(); } }); //启动两个线程 t1.start(); t2.start(); } }
- 测试结果:步奏1执行1次,步奏2执行两次,然后t1线程就会被t2线程终止
- 线程被打断会抛出错误:java.lang.InterruptedException: sleep interrupted
2、打断注入spring的线程
- 新建线程,注入spring中
- ps:注意,注入spring的线程是单例的,实际使用过程中,我们都是一个线程一个任务,如果需要不同的对象,可以开启spring的多例,见:SpringBoot实现多例模式
package com.cc.jschdemo.terminationThread; import lombok.Data; import lombok.EqualsAndHashCode; import org.springframework.stereotype.Component; /** * <p>注入spring的线程</p> * * @author -- * @since 2023/11/24 */ @EqualsAndHashCode(callSuper = true) @Data @Component public class SpringThread extends Thread{ //1可以注入Spring的Bean,然后直接使用 // @Resource // private IBasicInterruptService basicInterruptService; //2可以直接传入执行任务的参数(可多个) private String footwork; /** * 具体要执行的任务 */ @Override public void run() { try { //步奏1 Thread.sleep(1000); System.out.printf("完成:%s 1 %n", footwork); //步奏2(模仿阻塞) while (true) { Thread.sleep(1000); System.out.printf("完成:%s 2 %n", footwork); } }catch (InterruptedException e) { e.printStackTrace(); //捕获异常:InterruptedException,然后打断 //☆☆打断,终止当前线程☆☆ Thread.currentThread().interrupt(); } } }
- 创建任务,模仿打断
//可以直接注入线程 @Resource private SpringThread springThread; //模仿创建任务、打断 @Test public void test07()throws Exception{ try { ///传入执行任务,需要的参数 springThread.setFootwork("步奏"); //启动线程 springThread.start(); Thread.sleep(3100); System.out.println("打断线程!"); //打断t1线程 springThread.interrupt(); }catch (Exception e) { e.printStackTrace(); } }
- 结果:和《打断new出来的线程》一模一样
3、打断CompletableFuture启动的线程
- CompletableFuture的cancel(true)方法无法打断
- 调用线程的Thread task1,的interrupt也无法打断
- CompletableFuture.runAsync(task1)启动线程task1的底层逻辑是:CompletableFuture调用ForkJoinPool线程池启动一个线程(A)去执行task1任务。
- 所以我们要打断的不是task1,而是要打断A
package com.cc.jschdemo.terminationThread; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.CompletableFuture; /** * <p></p> * * @author -- * @since 2023/11/25 */ @RestController @RequestMapping("/completableFuture") public class CompletableFutureThread { //这里使用标记打断的话,每个CompletableFuture都需要一个标记,可以(CompletableFuture和标记)一起缓存下来。 boolean flagTask1 = false; boolean flagTask2 = false; //模仿任务1线程,也可以是:Runnable Thread task1 = new Thread(() -> { try { //模仿线程无限阻塞(while (true)) while (true) { System.out.println("任务1执行..."); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } //检查终止状态,如果终止就报错。true已中断;false未中断 // 这里获取到的是:CompletableFuture启动线程池的线程池的线程。就是执行task1的线程 boolean interrupted = Thread.currentThread().isInterrupted(); // 所以interrupted不能用来终止task1 //这里使用标记来终止task1(可以根据业务,在需要终止的地方设置这个打断。) if (flagTask1) { //打断:CompletableFuture启动线程池的线程池的线程 Thread.currentThread().interrupt(); } } }catch(Exception e){ e.printStackTrace(); } }); //任务2 Thread task2 = new Thread(() -> { try { //模仿线程阻塞 while (true) { System.out.println("任务2执行..."); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } //这里使用标记来终止task1 if (flagTask2) { Thread.currentThread().interrupt(); } } }catch(Exception e){ e.printStackTrace(); } }); //缓存CompletableFuture CompletableFuture<Void> future1; CompletableFuture<Void> future2; //启动任务 @GetMapping public void test08()throws Exception{ //实际使用中,可以使用循环添加CompletableFuture,然后记录下future(缓存),在要中断的逻辑中调用cancel //1开启task任务。这里实际是CompletableFuture调用ForkJoinPool线程池启动一个线程去执行task1任务。 //要想终止任务,要终止CompletableFuture调用ForkJoinPool线程池创建的线程,而非task1线程。 future1 = CompletableFuture.runAsync(task1); future2 = CompletableFuture.runAsync(task2); } //终止任务 @PostMapping public void stop() { //3取消任务 // 参数true表示尝试中断任务执行 // cancel方法会尝试取消任务的执行,参数true表示尝试中断任务执行。 // 成功取消返回true,否则返回false。 // 需要注意的是,cancel方法并不会直接中断正在执行的线程,而是会尝试取消任务的执行, // 具体是否成功取决于任务的实现。 //终止方式1:使用cancel终止:无法终止task1 boolean cancel1 = future1.cancel(Boolean.TRUE); System.out.println("任务1终止:" + (cancel1 ? "成功1" : "失败1")); //终止方式2:打断线程task1:无法终止task1 task1.interrupt(); //睡5s,让任务跑5s,看终止方式1、2是否能终止,测试结果为:不能终止 try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } //终止方式3:直接标记打断。CompletableFuture调用ForkJoinPool线程池生成的线程 // 真正的终止task1 flagTask1 = true; } }
-
测试
-
终止方式1/2都执行了。但是任务没有终止
- 终止方式3执行后,才真正的终止了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了