Java多線程之線程池使用
前言
學習使用線程池,而不是每次用線程的時候手動去創建,然後再進行銷燬,浪費系統資源。
對此線程池應運而生,將一些線程進行復用,循環使用,等下一次業務請求時候,提前創建好的線程對其做一些處理。
線程池簡介
爲了避免系統頻繁的創建和銷燬線程,我們可以將創建的線程進行復用,提前創建好一定數量的線程,對外直接提供使用,而不是等到想用的時候手動去創建線程,使用完成之後,再去銷燬這個線程,儘可能的複用創建的這些線程。
譬如
:數據庫中的數據庫連接池,tomcat連接池大小等。
線程池
就是首先創建一些線程,它們的集合稱爲線程池。使用線程池可以很好地提高性能,線程池在系統啓動時即創建大量空閒的線程,程序將一個任務傳給線程池,線程池就會啓動一條線程來執行這個任務,執行結束以後,該線程並不會死亡,而是再次返回線程池中成爲空閒狀態,等待執行下一個任務。
關於線程需要知道的事
- 使用多線程目的是爲了充分利用cpu併發多做事
- 線程並不是越多越好
- 線程創建,銷燬都很消耗資源
- 多線程不一定就快
使用線程池的好處
-
降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
-
提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
-
提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用線程池,必須對其實現原理瞭如指掌。
JDK中提供的線程池
爲了讓我們更好使用和管理線程池,在JDK1.5中 提供了 Executor 框架,處於java.util.concurrent
Executor 下的接口和類繼承關係
ExecutorService 接口中的方法
常用方法說明
-
shutdownNow:執行該方法,線程池的狀態立刻變成STOP狀態,並試圖停止所有正在執行的線程,不再處理還在池隊列中等待的任務,當然,它會返回那些未執行的任務。
它試圖終止線程的方法是通過調用Thread.interrupt()方法來實現的,但是大家知道,這種方法的作用有限,如果線程中沒有sleep 、wait、Condition、定時鎖等應用, interrupt()方法是無法中斷當前的線程的。所以,ShutdownNow()並不代表線程池就一定立即就能退出,它可能必須要等待所有正在執行的任務都執行完成了才能退出。 -
shutdown:當線程池調用該方法時,線程池的狀態則立刻變成SHUTDOWN狀態。此時,則不能再往線程池中添加任何任務,否則將會拋出RejectedExecutionException異常。但是,此時線程池不會立刻退出,直到添加到線程池中的任務都已經處理完成,纔會退出。
-
isShutDown:當調用shutdown()或shutdownNow()方法後返回爲true。
-
isTerminated:當調用shutdown()方法後,並且所有提交的任務完成後返回爲true;
-
isTerminated:當調用shutdownNow()方法後,成功停止後返回爲true;
-
submit:像線程池中提交任務,且帶返回值。
-
execute (父接口中的方法):像線程池中提交任務,無返回值。
創建線程池
創建線程時候一般使用java.util.concurrent 包中提供的工具類 Executors
創建線程池。
可以看到此類提供的一些方法
常用的有四個方法用來創建不同的線程池
public static ExecutorService newFixedThreadPool()
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool()
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newScheduledThreadPool()
-
newFixedThreadPool:該方法返回一個固定線程數量的線程池;
-
newSingleThreadExecutor:該方法返回一個只有一個現成的線程池;
-
newCachedThreadPool:返回一個可以根據實際情況調整線程數量的線程池;
-
newSingleThreadScheduledExecutor:該方法和 newSingleThreadExecutor 的區別是給定了時間執行某任務的功能,可以進行定時執行等;
-
newScheduledThreadPool:在newSingleThreadScheduledExecutor的基礎上可以指定線程數量。
經過查看源碼之後我們可以發現 上面四種創建線程方法調用的還是 ThreadPoolExecutor
在 Executors 內部創建線程池的時候,實際創建的都是一個 ThreadPoolExecutor 對象,只是對 ThreadPoolExecutor 構造方法,進行了默認值的設定。
ThreadPoolExecutor 的構造方法
其中這7個參數意義表示得含義是
1、corePoolSize 核心線程池大小;
當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads方法,線程池會提前創建並啓動所有基本線程。
2、maximumPoolSize 線程池最大容量大小;
線程池允許創建的最大線程數。如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。值得注意的是如果使用了無界的任務隊列這個參數就沒什麼效果。
3、keepAliveTime 線程池空閒時,線程存活的時間;
線程池的工作線程空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。
4、TimeUnit 時間單位;
可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
5、ThreadFactory 線程工廠
用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字。
默認是使用自帶的線程工廠方法創建線程
一些構造方法中也可自定義創建線程工廠,一般我們使用默認就可以。
6、BlockingQueue任務隊列;
用於保存等待執行的任務的阻塞隊列。 可以選擇以下幾個阻塞隊列。
- ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
- LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
- SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
- PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。
7、RejectedExecutionHandler 線程拒絕策略
如果放入LinkedBlockingQueue中的任務超過整型的最大數時,拋出RejectedExecutionException。
默認自帶的拒絕則側,直接拋出異常(有界隊列情況下),也可以自定義拒絕策略。
創建線程池
首先根據 Executors 創建線程池 Executors.newScheduledThreadPool(10);
但Idea中阿里守約提示以下信息,不建議使用Executors 去創建線程。
原因是因爲:這樣子創建線程,線程池中線程最大大小默認情況下是int最大取值大小的隊列。
- newFixedThreadPool和newSingleThreadExecutor:
主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至OOM。 - newCachedThreadPool和newScheduledThreadPool:
主要問題是線程數最大數是Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至OOM。
源碼如下
所以我們儘量使用 ThreadPoolExecutor
方法區創建線程池。
創建了一個核心線程是5最大線程是10 等待回收時間是60s 隊列長度是100的一個線程池
返回值得任務
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
ArrayList<String> list = new ArrayList();
for (int i = 0; i < 5; i++) {
Future<String> submit = poolExecutor.submit(() -> "---> " + new Random().nextInt(3000));
try {
list.add(submit.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
System.out.println("::" + poolExecutor.isTerminating());
poolExecutor.shutdown();
list.forEach(s -> System.out.println(s));
結果如下
—> 2321
—> 2957
—> 1381
—> 132
—> 1307
創建線程基本區別不大,唯一區別在於提交任務是否有返回值
-
execute() 方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功。通過以下代碼可知 execute() 方法輸入的任務是一個 Runnable 類的實例。
-
submit() 方法用於提交需要返回值的任務。線程池會返回一個 future 類型的對象,通過這個 future 對象可以判斷任務是否執行成功,並且可以通過 future 的 get() 方法來獲取返回值,get() 方法會阻塞當前線程直到任務完成,而使用 get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後立即返回,這時候有可能任務沒有執行完。