實現異步的九種方式,你知道幾個?
前言
大家好,我是田螺。
我們日常開發的時候,經常說到異步編程。比如說,在注冊接口,我們在用戶注冊成功時,用異步發送郵件通知用戶。
那么,小伙伴們,你知道實現異步編程,一共有多少種方式嗎?我給大家梳理了9種~~
- 使用Thread和Runnable
- 使用Executors提供線程池
- 使用自定義線程池
- 使用Future和Callable
- 使用CompletableFuture
- 使用ForkJoinPool
- Spring的@Async異步
- MQ實現異步
- 使用Hutool工具庫的ThreadUtil
1. 使用Thread和Runnable
Thread和Runnable是最基本的異步編程方式,直接使用Thread和Runnable來創建和管理線程。
public class Test {
public static void main(String[] args) {
System.out.println("田螺主線程:" + Thread.currentThread().getName());
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("田螺異步線程測試:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
但是呢,日常工作中,不推薦直接使用Thread和Runnable,因為它有這些缺點:
- 資源消耗大:每次創建新線程會消耗系統資源,頻繁創建和銷毀線程會導致性能下降。
- 難以管理:手動管理線程的生命周期、異常處理、任務調度等非常復雜。
- 缺乏擴展性:無法輕松控制并發線程的數量,容易導致系統資源耗盡。
- 線程復用問題:每次任務都創建新線程,無法復用已有的線程,效率低下。
2.使用Executors提供線程池
針對Thread和Runnable的缺點,我們可以使用線程池呀,線程池主要有這些優點:
- 它幫我們管理線程,避免增加創建線程和銷毀線程的資源損耗。因為線程其實也是一個對象,創建一個對象,需要經過類加載過程,銷毀一個對象,需要走GC垃圾回收流程,都是需要資源開銷的。
- 提高響應速度。如果任務到達了,相對于從線程池拿線程,重新去創建一條線程執行,速度肯定慢很多。
- 重復利用。線程用完,再放回池子,可以達到重復利用的效果,節省資源。
有些小伙伴說,我們可以直接使用Executors提供的線程池呀,非??旖莘奖?,比如:
- Executors.newFixedThreadPool
- Executors.newCachedThreadPool
簡單demo代碼如下:
public class Test {
public static void main(String[] args) {
System.out.println("田螺主線程:" + Thread.currentThread().getName());
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(()->{
System.out.println("田螺線程池方式,異步線程:" + Thread.currentThread().getName());
});
}
}
// 運行結果:
田螺主線程:main
田螺線程池方式,異步線程:pool-1-thread-1
3. 使用自定義線程池
使用使用Executors提供線程池,如newFixedThreadPool,雖然簡單快捷,但是呢,它的阻塞隊列十無界的!
newFixedThreadPool默認使用LinkedBlockingQueue作為任務隊列,而LinkedBlockingQueue是一個無界隊列(默認容量為Integer.MAX_VALUE)。如果任務提交速度遠大于線程池處理速度,隊列會不斷堆積任務,最終可能導致內存耗盡.
因此,我們一般推薦使用自定義線程池,來開啟異步。
public class Test {
public static void main(String[] args) {
// 自定義線程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心線程數
4, // 最大線程數
60, // 空閑線程存活時間
TimeUnit.SECONDS, // 時間單位
new ArrayBlockingQueue<>(8), // 任務隊列
Executors.defaultThreadFactory(), // 線程工廠
new ThreadPoolExecutor.CallerRunsPolicy() // 拒絕策略
);
System.out.println("田螺主線程:" + Thread.currentThread().getName());
executor.execute(() -> {
try {
Thread.sleep(500); // 模擬耗時操作
System.out.println("田螺自定義線程池開啟異步:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
//輸出
田螺主線程:main
田螺自定義線程池開啟異步:pool-1-thread-1
4. 使用Future和Callable
有些小伙伴說,如果我們期望異步編程可以返回結果呢?
那我們可以使用Future和Callable。Future和Callable是Java 5引入的,用于處理異步任務。Callable類似于Runnable,但它可以返回一個結果,并且可以拋出異常。Future表示異步計算的結果。
簡單使用demo:
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 自定義線程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心線程數
4, // 最大線程數
60, // 空閑線程存活時間
TimeUnit.SECONDS, // 時間單位
new ArrayBlockingQueue<>(8), // 任務隊列
Executors.defaultThreadFactory(), // 線程工廠
new ThreadPoolExecutor.CallerRunsPolicy() // 拒絕策略
);
System.out.println("田螺主線程:" + Thread.currentThread().getName());
Callable<String> task = () -> {
Thread.sleep(1000); // 模擬耗時操作
System.out.println("田螺自定義線程池開啟異步:" + Thread.currentThread().getName());
return "Hello, 公眾號:撿田螺的小男孩!";
};
Future<String> future = executor.submit(task);
String result = future.get(); // 阻塞直到任務完成
System.out.println("異步結果:"+result);
}
}
運行結果:
田螺主線程:main
田螺自定義線程池開啟異步:pool-1-thread-1
異步結果:Hello, 公眾號:撿田螺的小男孩!
5. 使用CompletableFuture
CompletableFuture是Java 8引入的,提供了更強大的異步編程能力,支持鏈式調用、異常處理、組合多個異步任務等。
簡單使用demo:
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 自定義線程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心線程數
4, // 最大線程數
60, // 空閑線程存活時間
TimeUnit.SECONDS, // 時間單位
new ArrayBlockingQueue<>(8), // 任務隊列
Executors.defaultThreadFactory(), // 線程工廠
new ThreadPoolExecutor.CallerRunsPolicy() // 拒絕策略
);
System.out.println("田螺主線程:" + Thread.currentThread().getName());
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000); // 模擬耗時操作
System.out.println("田螺CompletableFuture開啟異步:" + Thread.currentThread().getName());
return "Hello, 公眾號:撿田螺的小男孩!";
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello, 公眾號:撿田螺的小男孩!";
},executor);
future.thenAccept(result -> System.out.println("異步結果:" + result));
future.join();
}
}
6. 使用ForkJoinPool
有些時候,我們希望開啟異步,將一個大任務拆分成多個小任務(Fork),然后將這些小任務的結果合并(Join)。這時候,我們就可以使用ForkJoinPool啦~。
ForkJoinPool 是 Java 7 引入的一個線程池實現,專門用于處理分治任務。
- 它的特點就是任務拆分(Fork)和結果合并(Join),以及工作竊?。╓ork-Stealing)。
- ForkJoinPool 特別適合處理遞歸任務或可以分解的并行任務。
簡單使用demo:
public class Test {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(); // 創建 ForkJoinPool
int result = pool.invoke(new SumTask(1, 100)); // 提交任務并獲取結果
System.out.println("1 到 100 的和為: " + result);
}
static class SumTask extends RecursiveTask<Integer> {
private final int start;
private final int end;
SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= 10) { // 直接計算小任務
int sum = 0;
for (int i = start; i <= end; i++) sum += i;
return sum;
} else { // 拆分任務
int mid = (start + end) / 2;
SumTask left = new SumTask(start, mid);
SumTask right = new SumTask(mid + 1, end);
left.fork(); // 異步執行左任務
return right.compute() + left.join(); // 等待左任務完成并合并結果
}
}
}
}
7. Spring的@Async異步
Spring 提供了 @Async 注解來實現異步方法調用,非常方便。使用 @Async 可以讓方法在單獨的線程中執行,而不會阻塞主線程。
Spring @Async 的使用步驟其實很簡單:
- 啟用異步支持:在 Spring Boot項目中,需要在配置類或主應用類上添加 @EnableAsync 注解。
- 標記異步方法:在需要異步執行的方法上添加 @Async 注解。
- 配置線程池:默認情況下,Spring 使用一個簡單的線程池。如果需要自定義線程池,可以通過配置實現。
啟用異步支持:
@SpringBootApplication
@EnableAsync // 啟用異步支持
public class AsyncDemoApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncDemoApplication.class, args);
}
}
異步服務類
@Service
public class TianLuoAsyncService {
@Async // 標記為異步方法
public void asyncTianLuoTask() {
try {
Thread.sleep(2000); // 模擬耗時操作
System.out.println("異步任務完成,線程: " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
默認情況下,Spring 使用一個簡單的線程池(SimpleAsyncTaskExecutor),每次調用都會創建一個新線程。因此,在使用Spring的@Async進行異步時,推薦使用自定義線程池。
如下:
@Configuration
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心線程數
executor.setMaxPoolSize(20); // 最大線程數
executor.setQueueCapacity(50); // 隊列容量
executor.setThreadNamePrefix("AsyncThread-"); // 線程名前綴
executor.initialize();
return executor;
}
}
然后,在 @Async 注解中指定線程池的名稱:
@Async("taskExecutor") // 指定使用自定義的線程池
public void asyncTianLuoTask() {
// 方法邏輯
}
8. MQ實現異步
我們在提到MQ的時候,經常提到,它有異步處理、解耦、流量削鋒。是的,MQ經常用來實現異步編程。
簡要代碼如下:先保存用戶信息,然后發送注冊成功的MQ消息
// 用戶注冊方法
public void registerUser(String username, String email, String phoneNumber) {
// 保存用戶信息(簡化版)
userService.add(buildUser(username,email,phoneNumber))
// 發送消息
String registrationMessage = "User " + username + " has registered successfully.";
// 發送消息到隊列
rabbitTemplate.convertAndSend("registrationQueue", registrationMessage);
}
消費者從隊列中讀取消息并發送短信或郵件:
@Service
public class NotificationService {
// 監聽消息隊列中的消息并發送短信/郵件
@RabbitListener(queues = "registrationQueue")
public void handleRegistrationNotification(String message) {
// 這里可以進行短信或郵件的發送操作
System.out.println("Sending registration notification: " + message);
// 假設這里是發送短信的操作
sendSms(message);
// 也可以做其他通知(比如發郵件等)
sendEmail(message);
}
}
9.使用Hutool工具庫的ThreadUtil
可以使用的是類似 Hutool 工具庫中的 ThreadUtil,它提供了豐富的線程池管理和異步任務調度功能。
先引入依賴:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version> <!-- 請使用最新版本 -->
</dependency>
最簡單的,可以直接使用ThreadUtil.execute執行異步任務
public class Test {
public static void main(String[] args) {
System.out.println("田螺主線程");
ThreadUtil.execAsync(
() -> {
System.out.println("田螺異步測試:" + Thread.currentThread().getName());
}
);
}
}
//輸出
田螺主線程
田螺異步測試:pool-1-thread-1