什麼是池,我們在開發中經常會聽到有線程池啊,資料庫連接池等等。那麼到底什麼是池?其實很簡單,裝水的池子就叫水池嘛,用來裝線程的池子就叫線程池(廢話),就是我們把創建好的N個線程都放在一個池子裡面,如果有需要,我們就去取,不用額外的再去手動創建了為什麼要用線程池按照正常的想法是,我們需要一個線程,就去... ...
什麼是池,我們在開發中經常會聽到有線程池啊,資料庫連接池等等。
那麼到底什麼是池?
其實很簡單,裝水的池子就叫水池嘛,用來裝線程的池子就叫線程池(廢話),就是我們把創建好的N個線程都放在一個池子裡面,如果有需要,我們就去取,不用額外的再去手動創建了
為什麼要用線程池
按照正常的想法是,我們需要一個線程,就去創建一個線程,這樣的想法是沒錯的,但是如果需要有N多個線程呢?那把創建線程的代碼複製N多份?或者用for迴圈來創建?NO,這樣不是不行,但是不好。
因為線程也是有生命周期的,創建與銷毀線程都會對系統資源有很大的開銷,創建線程需要向系統申請相應的資源,銷毀線程又會對垃圾回收器造成壓力
使用線程池的好處
- 加快響應速度,需要線程我們就去取,不用額外的創建,可以反覆利用。
- 合理的利用CPU與記憶體資源,因為CPU與記憶體都不是無限的
- 可以把線程進行一個統一的管理
線程池的適用場景
在實際開發中,如果需要5個以上的線程,那麼就應該使用線程池來完成工作
線程池的創建與停止
我們先來說線程池的創建,我們都知道,在Java中的對象都是有構造方法的,有些可以使用無參的構造方法來創建,有些就需要使用有參的構造方法來創建,線程池的創建就必須要往構造方法中傳入參數,我們就先來瞭解一下線程池中的構造參數都是一些什麼含義吧,否則你怎麼知道你創建的線程池是一個什麼樣的運行規則呢
corePoolSize:(int)
核心線程數 --------------
線上程池創建後預設是沒有線程的,當有新的任務來的之後,線程池就會創建一個新的線程去執行這個任務
假設我們把這個參數設置為5,然後有5個任務提交了過來,就會創建5個線程去執行對應的任務
在任務執行完成之後,這5個線程並不會被收回,而是會一直保留線上程池中,等待下一個任務的到來
------------------
註意:當線程池中的線程數量少於我們設定的值時,不管之前創建的線程是否空閑,有新任務來時都會創建一個新的線程去執行workQueue:(BlockingQueue)
任務存儲隊列 -----------------
任務隊列有很多種:介紹常見的3種
1.SynchronousQueue:這種隊列內部是沒有容量的,任務過來後會直接轉交給線程去執行
2.LinkedBlockingQueue:無界隊列,沒有容量限制,如果記憶體足夠,可以無限擴展,如果線程處理任務的速度跟不上任務提交過來的速度,很容易造成記憶體浪費和記憶體溢出的現象
3.ArrayBolockingQueue:有界隊列,可以設置隊列的大小
-----------------
在我們核心線程數都被占滿並且都不空閑的時候,再有新的任務過來時,就會把新的任務存儲在任務隊列裡面maxPoolSize:(int)
最大線程數 ----------------
線程池中最大的線程數量,什麼意思呢,我們用一個案例來解釋它的作用
假設,核心線程數為5,任務隊列可以存儲的任務數量是100,最大線程數我們設置為10
當我們核心線程都不空閑時,而任務隊列又被堆滿了,也就是我們一共提交了105個任務過來,並且一個都沒有執行完
這個時候如果再有新的任務過來,那最大線程數就派上用場了。
這個時候,我們會額外的再去創建新的線程來執行新的任務,那額外的線程可以有多少呢
就是最大線程數-核心線程數之後得到的數量啦
如果線程池中的線程數量達到了最大線程數,再來新的任務,就會被拒絕
-------------------
註意:如果創建了額外的線程,那麼會先從隊列中取出位於隊列頭位置中的任務去執行,而不是新加進來的任務
額外創建的線程在任務執行完之後是會被銷毀的,並不會一直存在,這點跟核心線程數不同keepAliveTime:(long)
存活時間 ---------------------
這個存活時間就是說的額外線程執行完任務,空閑的時間超過了keepAliveTime之後,就會被回收了threadFactory:(ThreadFactory)
創建線程的工廠
Handler:(RejectedExecutionHandler)
拒絕策略
線程池應該手動創建還是自動創建
建議手動創建,可以更加明確線程池的運行規則,避免資源耗盡的情況
我們看看自動創建會帶來哪些問題
自動創建其實就是使用JDK已經創建好並提供給我們的一些線程池
newFixedThreadPool (中文意思:固定的線程池)
public class Main {
public static void main(String[] args) {
//創建一個newFixedThreadPool線程池,並設置它的線程數量為4
ExecutorService executorService = Executors.newFixedThreadPool(4);
//提交一千個任務去給它執行,結果就是它會不斷列印線程1-線程4的名字
for(int i = 0 ; i < 1000 ; i ++){
executorService.execute(new test());
}
}
}
//列印當前線程的名字
class test implements Runnable{
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
前面我們說了,線程池的構造函數有好幾個,為什麼這裡只要傳一個就行了,並且前面還說了,有核心線程數和最大線程數,滿足一定條件下會創建出額外的線程,那為什麼只會列印線程1到線程 4的名字呢,我們去看看這個線程池的源碼吧
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor( nThreads, //核心線程數
nThreads, //最大線程數
0L, //存活時間
TimeUnit.MILLISECONDS, //時間單位,這裡是毫秒
//任務隊列,這裡使用的是無界隊列
new LinkedBlockingQueue<Runnable>()
);
}
可以看出,內部new了一個ThreadPoolExecutor,裡面的參數我也給打上註釋了,核心線程數與最大線程數都是我們傳進來的4這個參數,所以,無論有多少個任務進來,最多也就會有4個線程在進行工作,同時我們也看到了這裡使用的無界隊列,無界隊列的特點就是沒有容量限制,只要記憶體足夠,可以無限擴展,所以不管有多少個任務進來,都會被存儲到任務隊列裡面去。
仔細思考一下,使用這種線程池會有什麼問題呢,當任務過多,線程處理不過來,就會不斷的堆積到任務隊列裡面,這就造成了記憶體浪費,當隊列向系統申請不到更多的記憶體時,還有新的任務提交過來,就會造成記憶體溢出。
可能會說,不是還有handler拒絕策略嘛,那我們再翻到前面看看,拒絕的條件是什麼,當核心線程數不夠用了,最大線程數也不夠用了,並且隊列已經滿了的時候,新的任務才會被拒絕,這裡是使用的無界隊列,就不存在隊列滿了這麼一個情況
newSingleThreadExecutor(單獨的線程池)
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for(int i = 0 ; i < 1000 ; i ++){
executorService.execute(new test());
}
}
}
class test implements Runnable{
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
這個線程池不用傳參數,從字面意思上就看看出,這個線程池裡面只有一個線程,我們看看它的源碼
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
可以看出,這個線程池與newFixedThreadPool線程池很類似,只不過一個需要我們來指定核心線程數與最大線程數,一個不需要指定,已經寫死了,就是一個,那麼原理也就跟newFixedThreadPool線程池是一樣的了。
newCechedThreadPool(緩存的線程池)
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i = 0 ; i < 1000 ; i ++){
executorService.execute(new test());
}
}
我們直接來看看它的源碼吧
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, //核心線程數
//最大線程數
Integer.MAX_VALUE,
//存活時間
60L, TimeUnit.SECONDS,
//直接交換隊列
new SynchronousQueue<Runnable>());
}
從源碼我們可以看出這種線程池的特性,首先,核心線程數是0,也就是說,這個線程池中沒有核心線程數,也就是沒有可以一直存活的線程,最大線程數為Integer類型的最大值,可以說是沒有上限的,你來多少任務我都接著,然後是存活時間,被設置為60秒,如果一個線程空閑的時間超過了60秒,就會被回收,然後是任務隊列,這個隊列前面介紹過了,裡面沒有容量,不會存放任務進去,每次一有任務就會立馬交給線程去處理。
這個線程池存在什麼問題,它會反覆的創建與銷毀線程,只要一有新的任務進來,就會立馬創建一個線程去執行這個任務,如果執行完後沒有新的任務交給它,就等待被銷毀(等死)。
其次,也是有可能造成記憶體溢出的錯誤的
newScheduledThreadPool(支持定時或周期性任務執行的線程池)
這個線程有兩種用法
我們先看第一種
public static void main(String[] args) {
//傳入一個int類型的參數,這個參數代表線程池中核心線程的數量
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
//調用schedule方法,第一個參數是我們要執行的任務,第二個參數時間,第三個時間單位
//就是說,線程池在間隔5秒之後去執行我們提交的任務
executorService.schedule(new test(), 5, TimeUnit.SECONDS);
}
第二種
public class Main {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
//間隔一秒後開始執行任務,然後每隔3秒再次執行
executorService.scheduleAtFixedRate(new test(), 1, 3, TimeUnit.SECONDS);
}
}
線程池中的線程數量設定為多少比較合適
根據自己的業務場景不同有不同的規則
CPU計算密集型的任務(計算、加密、Hash等),最佳的線程數應該為CPU核心的1-2倍
耗時IO型任務(讀寫資料庫、網路讀寫等),最佳的線程數可以為CPU核心數的很多倍,10、100甚至更多倍都可以的
計算公式:最佳線程數 = CPU核心數 * (1+平均等待時間/平均工作時間)
如何停止線程池
shutdown:優雅的停止線程
executorService.shutdown();
當我們調用了線程池的這個方法後,線程池就知道了我們需要它停止下來,同時,它並不會再去接收新的任務了
然後線程池就會把當前正在執行的任務和任務隊列中的任務都執行完後,進行停止
isShutdown
這個方法返回一個boolean值,就是當我們對線程池調用了shutdown之後,我們想知道它到底有沒有接收到
就可以調用這個方法,如果接收到了,會返回一個true,否則就是false
isTerminated
這個方法返回一個boolean值,這個方法用於檢測,整個線程池是否已經停止工作了
awaitTermination
executorService.awaitTermination(3l,TimeUnit.SECONDS);
這個方法與isTerminated不同,這個方法是說,在我等待的這個時間內,線程池是否已經結束工作了
如果結束返回ture,否則false
shutdownNow:暴力停止線程
調用這個方法後,不管線程池中的任務是否還在執行,也不管任務隊列中是否還有未執行的任務
線程池都會立刻停止工作,並且會把任務隊列中未執行的任務進行返回
List<Runnable> runnableList = executorService.shutdownNow();
任務太多,怎麼拒絕
拒絕的時機
- 當我們調用了shutdown方法後,如果還有新的線程進來,會直接拋出異常拒絕掉這個任務
- 當核心線程數與最大線程數都被占滿,並且任務隊列也滿了的時候,再有新的任務來也會被拒絕,這裡說的任務隊列是有界隊列,容量有限的
拒絕的策略
AbortPolicy:直接、暴力拒絕,就是簡單粗暴的拋出異常,還有新的任務?我不接收
DiscardPolicy:默默的丟棄,它不會去執行新的任務,然後也不告訴你它不執行,直接把任務丟掉,也不拋出異常
DiscardOldestPolicy:以舊換新,將任務隊列中存在最久沒有被執行的任務丟棄到,把新的任務添加進來
CallerRunsPolicy:讓提交這個任務的線程來執行這個任務
比方說,主線程向線程池中提交新的任務,但是線程池已經無法再接受新的任務了,它就會對主線程說,這個任務你來完成吧,然後主線程沒辦法,只好自己去執行這個任務
最後一種拒絕策略是最好的,因為前面三種策略都是有損失的,要麼新任務不執行,要麼拋棄舊的任務,而最後一種沒有損失,同時,這個線程老是給線程池去提交任務,那如果這個任務交由提交的線程去執行,那麼這個線程就沒有功夫去提交新的任務了,因為它已經被它提交的任務所占據了,只有等它的任務執行完之後,才會繼續提交,這同時也降低了任務的提交速度
Executor家族的辨析
我們在前面創建線程池的代碼中看到,一下是ExecutorService,一下又是Executors,那麼它們之間的關係到底是什麼呢,線程池不應該是ThreadPoolExecutor嗎?怎麼又是ExecutorService呢,別急,我們下麵會詳細介紹
我們從底層往上層講
Executor是最底層的,它是一個介面,它裡面只有一個方法:execute(Runnable command)
public interface Executor { void execute(Runnable command);
}ExecutorService也是一個介面,並繼承了Executor介面,同時還擴展了一些其它的方法
public interface ExecutorService extends Executor { void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
//這些方法我們在前面都已經介紹過了,這個介面聲明瞭一些對線程池進行管理的方法
}AbstractExecutorService是一個抽象類,它實現了ExecutorService,但它並沒有實現ExecutorService介面里的方法,因為它自己本身也是一個抽象類,所以可以不用去寫實現,它裡面只寫了一些自己的實例方法
public abstract class AbstractExecutorService implements ExecutorService
ThreadPoolExecutor:它才是最終的線程池類,它繼承了AbstractExecutorService,並實現了所有父類的方法
public class ThreadPoolExecutor extends AbstractExecutorService
好,上面的我們已經介紹清楚了,那麼Executors又是什麼呢,Executors其實是一個工具類,裡面有很多的方法,包括創建前面我們使用的那幾種線程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
這是我們前面所使用到的一種固定線程數的線程池,方法返回的是ExecutorService,
但是實際裡面返回的是ExecutorService的子類:ThreadPoolExecutor
使用線程池的註意事項
- 避免任務堆積
- 避免線程數過多
- 需要排查線程數,是否與預期一致,因為有時候線程不會被正常回收,有可能是我們的任務邏輯有問題,導致任務一直無法完成,線程一直無法停止工作等情況