線程池的概念
線程池是線程的一個緩存,可以提供線程重用,避免了線程重複地進行建立銷毀,更加科學地管理線程,對線程進行統一地優化,排程和監控。
适用于
任務時間短、任務量大。
如果一個線程任務時間非常 長,可以忽略到線程的建立和銷毀時間,那麼使用線程池反而不合适
阻塞隊列概念
阻塞隊列
在任意時刻,不管并發有多高,隻有一個一個線程能夠進行隊列的入隊或者出隊操作。線程安全。
隊列滿,隻能進行出隊,入隊被阻塞。
隊列空,隻能進行入隊,出隊被阻塞。
線程池的幾個重要參數
-
corePoolSize:核心線程數
核心線程會一直存活,即使沒有任務需要執行。
2 . maximumPoolSize 線程池最大線程數量
3 . keepAliveTime 空閑線程存活時間
一個線程如果處于空閑狀态,并且目前的線程數量大于corePoolSize,那麼在指定時間後,這個空閑線程會被銷毀,這裡的指定時間由keepAliveTime來設定
-
queueCapacity:任務隊列容量(阻塞隊列)
當線程數達到最大時,新任務會放在隊列中排隊等待執行。
線程池的執行順序
線程池按以下行為執行任務
- 當線程數小于核心線程數時,建立線程。
- 當線程數大于等于核心線程數,且任務隊列未滿時,将任務放入任務隊列。
-
當線程數大于等于核心線程數,且任務隊列已滿
若線程數小于最大線程數,建立線程
若線程數等于最大線程數,抛出異常,拒絕任務
是以并不能保證先來的先執行
線程池在springboot中的使用
線程池的五種狀态
Running
能接受新任務以及處理已經添加的任務
Shutdown
不接受新任務,但可以處理已經添加的任務
Stop
不接受新任務,不處理已經添加的任務,并且中斷正在執行的任務
Tidying
所有任務已經終止
Terminated
線程池徹底終止
Spring中線程池
Spring中實作多線程,其實非常簡單,隻需要在配置類中添加@EnableAsync就可以使用多線程。在希望執行的并發方法中使用@Async就可以定義一個線程任務。通過spring給我們提供的ThreadPoolTaskExecutor就可以使用線程池。
線程池ThreadPoolExecutor,它的執行規則如下
在Springboot中對其進行了簡化處理,隻需要配置一個類型為java.util.concurrent.TaskExecutor或其子類的bean,并在配置類或直接在程式入口類上聲明注解@EnableAsync。
調用也簡單,在由Spring管理的對象的方法上标注注解@Async,顯式調用即可生效。
第一步,先在Spring Boot主類中定義一個線程池
例子:
@Configuration
@EnableAsync // 啟用異步任務
public class AsyncConfiguration {
// 聲明一個線程池(并指定線程池的名字)
@Bean("taskExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心線程數5:線程池建立時候初始化的線程數
executor.setCorePoolSize(5);
//最大線程數5:線程池最大的線程數,隻有在緩沖隊列滿了之後才會申請超過核心線程數的線程
executor.setMaxPoolSize(5);
//緩沖隊列500:用來緩沖執行任務的隊列
executor.setQueueCapacity(500);
//允許線程的空閑時間60秒:當超過了核心線程出之外的線程在空閑時間到達之後會被銷毀
executor.setKeepAliveSeconds(60);
//線程池名的字首:設定好了之後可以友善我們定位處理任務所在的線程池
executor.setThreadNamePrefix("DailyAsync-");
executor.initialize();
return executor;
}
}
有很多可以配置的東西。預設情況下,使用SimpleAsyncTaskExecutor
第二步,使用線程池
在定義了線程池之後,我們隻需要在@Async注解中指定線程池名即可,比如:
例子:
```java
@Service
public class GitHubLookupService {
private static final Logger logger = LoggerFactory.getLogger(GitHubLookupService.class);
@Autowired
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
Thread.sleep(3000L);
return CompletableFuture.completedFuture(results);
}
}
findUser 方法被标記為Spring的 @Async 注解,表示它将在一個單獨的線程上運作。該方法的傳回類型是 CompleetableFuture 而不是 String,這是任何異步服務的要求。
#### 高并發下線程池配置
線程池大小配置
一般根據任務類型進行區分, 假設CPU為N核
CPU密集型任務需要減少線程數量, 降低線程切換開銷,可配置線程池大小為N + 1.
IO密集型任務則可以加大線程數量, 可配置線程池大小為 N * 2.
混合型任務則可以拆分為CPU密集型與IO密集型, 獨立配置.
CPU密集型:
例如,一般我們系統的靜态資源,比如js,css等,會存在一個版本号,如 main.js?v0,每當使用者通路這個資源的時候,會發送一個比對請求到服務端,比對本地靜态檔案版本和服務端的檔案版本是否一緻,不一緻則更新.這種任務一般不占用大量IO,是以背景伺服器可以快速處理,壓力落在CPU上.
I/O密集型
1個線程對應1個方法棧,線程的生命周期與方法棧相同.
比如某個線程的方法棧對應的入站順序為:controller()->service()->DAO(),由于DAO長時間的I/O操作,導緻該線程一直處于工作隊列,但它又不占用CPU,則此時有1個CPU是處于空閑狀态的.
是以,這種情況下,應該加大線程池工作隊列的長度(如果CPU排程算法使用的是FCFS,則無法切換),盡量不讓CPU空閑下來,提高CPU使用率.
## 實戰
啟動類添加@EnableAsync , 表示開啟異步任務
使用@Configuration 配置一個線程池
@Configuration
@EnableAsync
public class AsynConfig {
@Bean("taskExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心線程數5:線程池建立時候初始化的線程數
executor.setCorePoolSize(5);
//最大線程數5:線程池最大的線程數,隻有在緩沖隊列滿了之後才會申請超過核心線程數的線程
executor.setMaxPoolSize(5);
//緩沖隊列500:用來緩沖執行任務的隊列
executor.setQueueCapacity(500);
//允許線程的空閑時間60秒:當超過了核心線程出之外的線程在空閑時間到達之後會被銷毀
executor.setKeepAliveSeconds(60);
//線程池名的字首:設定好了之後可以友善我們定位處理任務所在的線程池
executor.setThreadNamePrefix("DailyAsync-");
executor.initialize();
return executor;
}
}
給異步方法@Async(“taskExcutor”)
@Async("taskExecutor")
public Future<Map<String, Object>> query(User d, HttpServletRequest request) {
// TODO Auto-generated method stub
//省略邏輯
Map<String,Object> map = new HashMap<>();
return new AsyncResult<>(map);
}
異步回調 給方法外面包一層 Future<> 傳回值 AsyncResult
回調方法調用AsynResult.get()方法
get方法會阻塞到調用結束
resultMap.put("query", quary.get(10, TimeUnit.SECONDS));
### 注意點
1.注解的方法必須是public方法。
2.方法一定要從另一個類中調用,也就是從類的外部調用,類的内部調用是無效的。
3.如果需要從類的内部調用,需要先擷取其代理類,下面上代碼
```java
@Service
public class XxxService{
public void methodA(){
...
XxxService xxxServiceProxy = SpringUtil.getBean(XxxService.class);
xxxServiceProxy.methodB();
...
}
@Async
public void methodB() {
...
}
}
代理工具類我也貼在下面
@Component
public class SpringBeanUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringBeanUtil.applicationContext == null) {
SpringBeanUtil.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext(){
return applicationContext;
}
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}
ReportService beanProxy = SpringBeanUtil.getBean(ReportService.class);
Future<JSONObject> reportAsync = beanProxy.getReportAsync(patCardType, patCardNo, beginDate, endDate);
這樣就可以在同一個類中調用異步方法