多線程編程之——終止(打斷)正在執行中的線程 ps:文字有點多,想看結果的,直接跳轉:《二》 一、基礎知識 1、我們基於spring開發,把線程都交給spring 把線程交給spring管理好不好? 將線程交給Spring管理是一個常見的做法,特別是在基於Spring的應用程式中。通過將線程納入Sp ...
多線程編程之——終止(打斷)正在執行中的線程
- 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執行後,才真正的終止了。