目錄
二、ThreadPoolTaskExecutor和ThreadPoolExecutor區別
一、SpringBoot異步線程池
1、定義線程池
代碼示例:配置一個線程池,這裏使用spring封裝的線程池
@EnableAsync // 開啓異步任務
@Configuration
public class TaskPoolConfig {
@Bean("taskExecutor") // 線程池名稱
public Executor taskExecutor() {
// 使用Spring封裝的異步線程池
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 初始化線程數
executor.setMaxPoolSize(20); // 最大線程數
executor.setQueueCapacity(200); // 緩衝隊列
executor.setKeepAliveSeconds(60); // 允許空閒時間/秒
executor.setThreadNamePrefix("taskExecutor-");// 線程池名前綴-方便日誌查找
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize(); // 初始化
return executor;
}
}
上面我們通過使用ThreadPoolTaskExecutor
創建了一個線程池,同時設置了以下這些參數:
- 核心線程數10:線程池創建時候初始化的線程數
- 最大線程數20:線程池最大的線程數,只有在緩衝隊列滿了之後纔會申請超過核心線程數的線程
- 緩衝隊列200:用來緩衝執行任務的隊列
- 允許線程的空閒時間60秒:當超過了核心線程出之外的線程在空閒時間到達之後會被銷燬
- 線程池名的前綴:設置好了之後可以方便我們定位處理任務所在的線程池
- 線程池對拒絕任務的處理策略:這裏採用了
CallerRunsPolicy
策略,當線程池沒有處理能力的時候,該策略會直接在 execute 方法的調用線程中運行被拒絕的任務;如果執行程序已關閉,則會丟棄該任務
說明:setWaitForTasksToCompleteOnShutdown(true)
該方法就是這裏的關鍵,用來設置線程池關閉的時候等待所有任務都完成再繼續銷燬其他的Bean,這樣這些異步任務的銷燬就會先於Redis線程池的銷燬。同時,這裏還設置了setAwaitTerminationSeconds(60),該方法用來設置線程池中任務的等待時間,如果超過這個時候還沒有銷燬就強制銷燬,以確保應用最後能夠被關閉,而不是阻塞住。
2、線程池的使用
使用多線程,往往是創建Thread,或者是實現runnable接口,用到線程池的時候還需要創建Executors,spring中有十分優秀的支持,就是註解@EnableAsync就可以使用多線程,@Async加在線程任務的方法上(需要異步執行的任務),定義一個線程任務,通過spring提供的ThreadPoolTaskExecutor就可以使用線程池。
線程池的使用在Spring中非常簡單,只要設置兩個註解就可以了
(1)@EnableAsync // 開啓異步任務
(2)@Async("taskExecutor") // 申明爲異步方法,指定線程池名稱
注: @Async所修飾的函數不要定義爲static類型,這樣異步調用不會生效
@Slf4j
@Component
public class Task {
public static Random random = new Random();
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Async("taskExecutor")
public void doTaskOne() throws Exception {
log.info("開始做任務一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info(stringRedisTemplate.randomKey());
log.info("完成任務一,耗時:" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskTwo() throws Exception {
log.info("開始做任務二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任務二,耗時:" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskThree() throws Exception {
log.info("開始做任務三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任務三,耗時:" + (end - start) + "毫秒");
}
}
簡單測試下:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class TaskTest {
@Autowired
private Task task;
@Test
public void test() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
Thread.currentThread().join();
}
}
測試結果如下:
2020-04-16 15:23:13.834 INFO 1828 --- [ taskExecutor-1] demo.spring.tasks.Task : 開始做任務一
2020-04-16 15:23:13.834 INFO 1828 --- [ taskExecutor-2] demo.spring.tasks.Task : 開始做任務二
2020-04-16 15:23:13.835 INFO 1828 --- [ taskExecutor-3] demo.spring.tasks.Task : 開始做任務三
2020-04-16 15:23:17.539 INFO 1828 --- [ taskExecutor-2] demo.spring.tasks.Task : 完成任務二,耗時:3704毫秒
2020-04-16 15:23:18.380 INFO 1828 --- [ scheduling-1] demo.spring.tasks.ScheduledTasks : ScheduledTasks1 - The time is now 15:23:18
2020-04-16 15:23:18.381 INFO 1828 --- [ scheduling-1] demo.spring.tasks.ScheduledTasks : ScheduledTasks2 - The time is now 15:23:18
2020-04-16 15:23:19.475 INFO 1828 --- [ taskExecutor-3] demo.spring.tasks.Task : 完成任務三,耗時:5640毫秒
其中任務一會報錯,因爲沒有配置相關的redis配置,但是並不影響其他任務的執行。
二、ThreadPoolTaskExecutor和ThreadPoolExecutor區別
ThreadPoolTaskExecutor是Spring對ThreadPoolExecutor進行封裝,它實現方式完全是使用threadPoolExecutor進行實現,來看一下源碼
public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport implements SchedulingTaskExecutor {
private final Object poolSizeMonitor = new Object();
private int corePoolSize = 1;
private int maxPoolSize = 2147483647;
private int keepAliveSeconds = 60;
private boolean allowCoreThreadTimeOut = false;
private int queueCapacity = 2147483647;
private ThreadPoolExecutor threadPoolExecutor; //這裏就用到了ThreadPoolExecutor
瞭解了ThreadPoolTaskExecutor的相關情況,接下來看一下ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- int corePoolSize:線程池維護線程的最小數量.
- int maximumPoolSize:線程池維護線程的最大數量.
- long keepAliveTime:空閒線程的存活時間.
- TimeUnit unit: 時間單位,現有納秒,微秒,毫秒,秒枚舉值.
- BlockingQueue<Runnable> workQueue:持有等待執行的任務隊列.
-
threadFactory(線程工廠):用於創建新線程。threadFactory創建的線程也是採用new Thread()方式,threadFactory創建的線程名都具有統一的風格:pool-m-thread-n(m爲線程池的編號,n爲線程池內的線程編號)
- RejectedExecutionHandler handler: 線程飽和策略,用來拒絕一個任務的執行.
RejectedExecutionHandler handler: 用來拒絕一個任務的執行,有兩種情況會發生這種情況:
一是在execute方法中若addIfUnderMaximumPoolSize(command)爲false,即線程池已經飽和;
二是在execute方法中, 發現runState!=RUNNING || poolSize == 0,即已經shutdown,就調用ensureQueuedTaskHandled(Runnable command),在該方法中有可能調用reject。
1、ThreadPoolExecutor的處理流程
1)當池子大小小於corePoolSize就新建線程,並處理請求
2)當池子大小等於corePoolSize,把請求放入workQueue中,池子裏的空閒線程就去從workQueue中取任務並處理
3)當workQueue放不下新入的任務時,新建線程入池,並處理請求,如果池子大小撐到了maximumPoolSize就用RejectedExecutionHandler來做拒絕處理
4)另外,當池子的線程數大於corePoolSize的時候,多餘的線程會等待keepAliveTime長的時間,如果無請求可處理就自行銷燬
總結下:
ThreadPoolExecutor會優先創建 CorePoolSiz 線程, 當繼續增加線程時,先放入Queue中,當 CorePoolSiz 和 Queue 都滿的時候,就增加創建新線程,當線程達到MaxPoolSize的時候,就會拋出錯誤 org.springframework.core.task.TaskRejectedException
另外MaxPoolSize的設定如果比系統支持的線程數還要大時,會拋出java.lang.OutOfMemoryError: unable to create new native thread 異常。
2、四種Reject預定義策略
(1)ThreadPoolExecutor.AbortPolicy策略,是默認的策略,處理程序遭到拒絕將拋出運行時 RejectedExecutionException。
(2)ThreadPoolExecutor.CallerRunsPolicy策略 ,調用者的線程會執行該任務,如果執行器已關閉,則丟棄。
(3)ThreadPoolExecutor.DiscardPolicy策略,不能執行的任務將被丟棄。
(4)ThreadPoolExecutor.DiscardOldestPolicy策略,如果執行程序尚未關閉,則位於工作隊列頭部的任務將被刪除,然後重試執行程序(如果再次失敗,則重複此過程)。
三、Java線程池
1、使用線程池的優勢
- 降低系統資源消耗,通過重用已存在的線程,降低線程創建和銷燬造成的消耗;
- 提高系統響應速度,當有任務到達時,通過複用已存在的線程,無需等待新線程的創建便能立即執行;
- 方便線程併發數的管控。因爲線程若是無限制的創建,可能會導致內存佔用過多而產生OOM,並且會造成cpu過度切換(cpu切換線程是有時間成本的(需要保持當前執行線程的現場,並恢復要執行線程的現場))。
- 提供更強大的功能,延時定時線程池。
2、什麼是阻塞隊列?
阻塞隊列BlockingQueue,相當我們經常接觸的List,但如果BlockQueue是空的,這時如果有線程要從這個BlockingQueue取元素的時候將會被阻塞進入等待狀態,直到別的線程在BlockingQueue中添加進了元素,被阻塞的線程纔會被喚醒。同樣,如果BlockingQueue是滿的,試圖往隊列中存放元素的線程也會被阻塞進入等待狀態,直到BlockingQueue裏的元素被別的線程拿走纔會被喚醒繼續操作。
3、線程池爲什麼要是使用阻塞隊列?
阻塞隊列可以保證任務隊列中沒有任務時阻塞獲取任務的線程,使得線程進入wait狀態,釋放cpu資源。當隊列中有任務時才喚醒對應線程從隊列中取出消息進行執行。使得線程不至於一直佔用cpu資源。
線程執行完任務後通過循環再次從任務隊列中取出任務進行執行,代碼片段如下while (task != null || (task = getTask()) != null) {...}。
不用阻塞隊列也是可以的,不過實現起來比較麻煩而已,有好用的爲啥不用呢?
4、如何配置線程池?
(1)CPU密集型任務
儘量使用較小的線程池,一般爲CPU核心數+1。 因爲CPU密集型任務使得CPU使用率很高,若開過多的線程數,會造成CPU過度切換。
(2)IO密集型任務
可以使用稍大的線程池,一般爲2*CPU核心數。 IO密集型任務CPU使用率並不高,因此可以讓CPU在等待IO的時候有其他線程去處理別的任務,充分利用CPU時間。
(3)混合型任務
可以將任務分成IO密集型和CPU密集型任務,然後分別用不同的線程池去處理。 只要分完之後兩個任務的執行時間相差不大,那麼就會比串行執行來的高效。
因爲如果劃分之後兩個任務執行時間有數據級的差距,那麼拆分沒有意義。
因爲先執行完的任務就要等後執行完的任務,最終的時間仍然取決於後執行完的任務,而且還要加上任務拆分與合併的開銷,得不償失。
5、Java中提供的線程池
Executors類提供了4種不同的線程池:newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor
(1)newCachedThreadPool
用來創建一個可以無限擴大的線程池,適用於負載較輕的場景,執行短期異步任務。(可以使得任務快速得到執行,因爲任務時間執行短,可以很快結束,也不會造成cpu過度切換)
(2)newFixedThreadPool
創建一個固定大小的線程池,因爲採用無界的阻塞隊列,所以實際線程數量永遠不會變化,適用於負載較重的場景,對當前線程數量進行限制。(保證線程數可控,不會造成線程過多,導致系統負載更爲嚴重)
(3)newSingleThreadExecutor
創建一個單線程的線程池,適用於需要保證順序執行各個任務。
(4)newScheduledThreadPool
適用於執行延時或者週期性任務。