一、楔子
在我們的系統中,經常會處理一些耗時任務,自然而然的會想到使用多線程,JDK給我們提供了非常方便的操作線程的API,為什么還要使用Spring來實現多線程呢?
1.使用Spring比使用JDK原生的並發API更簡單。(一個注解@Async就搞定)
2.我們的應用環境一般都會集成Spring,我們的Bean也都交給Spring來進行管理,那么使用Spring來實現多線程更加簡單,更加優雅。
為什么要用異步?當需要調用多個服務時,使用傳統的同步調用來執行時,是這樣的
如果每個服務需要3秒的響應時間,這樣順序執行下來,可能需要9秒以上才能完成業務邏輯,但是如果我們使用異步調用
調用服務A
調用服務B
調用服務C
然后等待從服務A、B和C的響應
根據從服務A、服務B和服務C返回的數據完成業務邏輯,然后結束
理論上 3秒左右即可完成同樣的業務邏輯
二、spring boot 如何使用多線程
Spring中實現多線程,其實非常簡單,只需要在配置類中添加@EnableAsync就可以使用多線程。在希望執行的並發方法中使用@Async就可以定義一個線程任務。通過spring給我們提供的ThreadPoolTaskExecutor就可以使用線程池。
2.1 第一步,先在Spring Boot主類中定義一個線程池
package com.godfreyy.springbootasync.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* 異步任務配置類
*
* @author godfrey
* @since 2021-12-16
*/
@Configuration
@EnableAsync // 啟用異步任務
public class AsyncConfiguration {
/**
* 聲明一個線程池(並指定線程池的名字)
*
* @param
* @return Executor
*/
@Bean("taskExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心線程數3:線程池創建時候初始化的線程數
executor.setCorePoolSize(3);
//最大線程數5:線程池最大的線程數,只有在緩沖隊列滿了之后才會申請超過核心線程數的線程
executor.setMaxPoolSize(5);
//緩沖隊列500:用來緩沖執行任務的隊列
executor.setQueueCapacity(500);
//允許線程的空閑時間60秒:當超過了核心線程出之外的線程在空閑時間到達之后會被銷毀
executor.setKeepAliveSeconds(60);
//線程池名的前綴:設置好了之后可以方便我們定位處理任務所在的線程池
executor.setThreadNamePrefix("DailyAsync-");
executor.initialize();
return executor;
}
}
有很多你可以配置的東西。默認情況下,使用SimpleAsyncTaskExecutor。
2.2 第二步,使用線程池
在定義了線程池之后,我們如何讓異步調用的執行任務使用這個線程池中的資源來運行呢?方法非常簡單,我們只需要在@Async注解中指定線程池名即可,比如:
package com.godfreyy.springbootasync.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* @author godfrey
* @since 2021-12-16
*/
@Service
public class GitHubLookupService {
private static final Logger logger = LoggerFactory.getLogger(GitHubLookupService.class);
@Resource
private RestTemplate restTemplate;
// 這里進行標注為異步任務,在執行此方法的時候,會單獨開啟線程來執行(並指定線程池的名字)
@Async("taskExecutor")
public CompletableFuture<String> findUser(String user) throws InterruptedException {
logger.info("Looking up " + user);
String url = String.format("https://api.github.com/users/%s", user);
String results = restTemplate.getForObject(url, String.class);
// Artificial delay of 3s for demonstration purposes
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("sleep...");
} catch (InterruptedException e) {
e.printStackTrace();
}
return CompletableFuture.completedFuture(results);
}
}
findUser 方法被標記為Spring的 @Async 注解,表示它將在一個單獨的線程上運行。該方法的返回類型是 CompleetableFuture 而不是 String,這是任何異步服務的要求。
2.3 第三步,單元測試
最后,我們來寫個單元測試來驗證一下
package com.godfreyy.springbootasync;
import com.godfreyy.springbootasync.service.GitHubLookupService;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@SpringBootTest
class SpringbootAsyncApplicationTests {
private static final Logger logger = LoggerFactory.getLogger(SpringbootAsyncApplicationTests.class);
@Resource
private GitHubLookupService gitHubLookupService;
@Test
public void asyncTest() throws InterruptedException, ExecutionException {
// Start the clock
long start = System.currentTimeMillis();
// Kick of multiple, asynchronous lookups
CompletableFuture<String> page1 = gitHubLookupService.findUser("PivotalSoftware");
CompletableFuture<String> page2 = gitHubLookupService.findUser("CloudFoundry");
CompletableFuture<String> page3 = gitHubLookupService.findUser("Spring-Projects");
// Wait until they are all done
//join() 的作用:讓“主線程”等待“子線程”結束之后才能繼續運行
CompletableFuture.allOf(page1, page2, page3).join();
// Print results, including elapsed time
float exc = (float)(System.currentTimeMillis() - start)/1000;
logger.info("Elapsed time: " + exc + " seconds");
logger.info("--> " + page1.get());
logger.info("--> " + page2.get());
logger.info("--> " + page3.get());
}
}
執行上面的單元測試,我們可以在控制台中看到所有輸出的線程名前都是之前我們定義的線程池前綴名開始的,並且執行時間小於9秒,說明我們使用線程池來執行異步任務的試驗成功了!
三、注意事項
在使用spring的異步多線程時經常回碰到多線程失效的問題,解決方式為:
異步方法和調用方法一定要寫在不同的類中 ,如果寫在一個類中,是沒有效果的!
原因:
spring對@Transactional注解時也有類似問題,spring掃描時具有@Transactional注解方法的類時,是生成一個代理類,由代理類去開啟關閉事務,而在同一個類中,方法調用是在類體內執行的,spring無法截獲這個方法調用。