什么是線程池?
Java中的線程池是運用場景最多的并發(fā)框架,幾乎所有需要異步或并發(fā)執(zhí)行任務(wù)的程序
都可以使用線程池。在開發(fā)過程中,合理地使用線程池能夠帶來3個好處。
第一:降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
第二:提高響應(yīng)速度。當任務(wù)到達時,任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行。
第三:提高線程的可管理性。線程是稀缺資源,如果無限制地創(chuàng)建,不僅會消耗系統(tǒng)資源,
還會降低系統(tǒng)的穩(wěn)定性,使用線程池可以進行統(tǒng)一分配、調(diào)優(yōu)和監(jiān)控。但是,要做到合理利用
線程池,必須對其實現(xiàn)原理了如指掌。
線程池作用
線程池是為突然大量爆發(fā)的線程設(shè)計的,通過有限的幾個固定線程為大量的操作服務(wù),減少了創(chuàng)建和銷毀線程所需的時間,從而提高效率。
如果一個線程的時間非常長,就沒必要用線程池了(不是不能作長時間操作,而是不宜。),況且我們還不能控制線程池中線程的開始、掛起、和中止。
線程池的分類
ThreadPoolExecutor
Java是天生就支持并發(fā)的語言,支持并發(fā)意味著多線程,線程的頻繁創(chuàng)建在高并發(fā)及大數(shù)據(jù)量是非常消耗資源的,因為java提供了線程池。在jdk1.5以前的版本中,線程池的使用是及其簡陋的,但是在JDK1.5后,有了很大的改善。JDK1.5之后加入了java.util.concurrent包,
java.util.concurrent包的加入給予開發(fā)人員開發(fā)并發(fā)程序以及解決并發(fā)問題很大的幫助。這篇文章主要介紹下并發(fā)包下的Executor接口,Executor接口雖然作為一個非常舊的接口(JDK1.5 2004年發(fā)布),但是很多程序員對于其中的一些原理還是不熟悉,因此寫這篇文章來介紹下Executor接口,同時鞏固下自己的知識。如果文章中有出現(xiàn)錯誤,歡迎大家指出。
Executor框架的最頂層實現(xiàn)是ThreadPoolExecutor類,Executors工廠類中提供的newScheduledThreadPool、newFixedThreadPool、newCachedThreadPool方法其實也只是ThreadPoolExecutor的構(gòu)造函數(shù)參數(shù)不同而已。通過傳入不同的參數(shù),就可以構(gòu)造出適用于不同應(yīng)用場景下的線程池,那么它的底層原理是怎樣實現(xiàn)的呢,這篇就來介紹下ThreadPoolExecutor線程池的運行過程。
corePoolSize: 核心池的大小。 當有任務(wù)來之后,就會創(chuàng)建一個線程去執(zhí)行任務(wù),當線程池中的線程數(shù)目達到corePoolSize后,就會把到達的任務(wù)放到緩存隊列當中
maximumPoolSize: 線程池最大線程數(shù),它表示在線程池中最多能創(chuàng)建多少個線程;
keepAliveTime: 表示線程沒有任務(wù)執(zhí)行時最多保持多久時間會終止
unit: 參數(shù)keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態(tài)屬性:
線程池四種創(chuàng)建方式
Java通過Executors(jdk1.5并發(fā)包)提供四種線程池,分別為:
newCachedThreadPool創(chuàng)建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
newFixedThreadPool 創(chuàng)建一個定長線程池,可控制線程最大并發(fā)數(shù),超出的線程會在隊列中等待。
newScheduledThreadPool 創(chuàng)建一個定長線程池,支持定時及周期性任務(wù)執(zhí)行。
newSingleThreadExecutor 創(chuàng)建一個單線程化的線程池,它只會用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行。
1.newCachedThreadPool
創(chuàng)建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。示例代碼如下:
// 無限大小線程池 jvm自動回收
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int temp = i;
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName() + ",i:" + temp);
}
});
}
總結(jié): 線程池為無限大,當執(zhí)行第二個任務(wù)時第一個任務(wù)已經(jīng)完成,會復(fù)用執(zhí)行第一個任務(wù)的線程,而不用每次新建線程。
2.newFixedThreadPool
創(chuàng)建一個定長線程池,可控制線程最大并發(fā)數(shù),超出的線程會在隊列中等待。示例代碼如下:
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
final int temp = i;
newFixedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + ",i:" + temp);
}
});
}
總結(jié):因為線程池大小為3,每個任務(wù)輸出index后sleep 2秒,所以每兩秒打印3個數(shù)字。
定長線程池的大小最好根據(jù)系統(tǒng)資源進行設(shè)置。如Runtime.getRuntime().availableProcessors()
3.newScheduledThreadPool
創(chuàng)建一個定長線程池,支持定時及周期性任務(wù)執(zhí)行。延遲執(zhí)行示例代碼如下:
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 10; i++) {
final int temp = i;
newScheduledThreadPool.schedule(new Runnable() {
public void run() {
System.out.println("i:" + temp);
}
}, 3, TimeUnit.SECONDS);
}
表示延遲3秒執(zhí)行。
4.newSingleThreadExecutor
創(chuàng)建一個單線程化的線程池,它只會用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行。示例代碼如下:
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
newSingleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("index:" + index);
try {
Thread.sleep(200);
} catch (Exception e) {
// TODO: handle exception
}
}
});
}
注意: 結(jié)果依次輸出,相當于順序執(zhí)行各個任務(wù)。
線程池原理剖析
提交一個任務(wù)到線程池中,線程池的處理流程如下:
1、判斷線程池里的核心線程是否都在執(zhí)行任務(wù),如果不是(核心線程空閑或者還有核心線程沒有被創(chuàng)建)則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù)。如果核心線程都在執(zhí)行任務(wù),則進入下個流程。
2、線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務(wù)存儲在這個工作隊列里。如果工作隊列滿了,則進入下個流程。
3、判斷線程池里的線程是否都處于工作狀態(tài),如果沒有,則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù)。如果已經(jīng)滿了,則交給飽和策略來處理這個任務(wù)。
自定義線程線程池
如果當前線程池中的線程數(shù)目小于corePoolSize,則每來一個任務(wù),就會創(chuàng)建一個線程去執(zhí)行這個任務(wù);
如果當前線程池中的線程數(shù)目>=corePoolSize,則每來一個任務(wù),會嘗試將其添加到任務(wù)緩存隊列當中,若添加成功,則該任務(wù)會等待空閑線程將其取出去執(zhí)行;若添加失?。ㄒ话銇碚f是任務(wù)緩存隊列已滿),則會嘗試創(chuàng)建新的線程去執(zhí)行這個任務(wù);
如果隊列已經(jīng)滿了,則在總線程數(shù)不大于maximumPoolSize的前提下,則創(chuàng)建新的線程
如果當前線程池中的線程數(shù)目達到maximumPoolSize,則會采取任務(wù)拒絕策略進行處理;
如果線程池中的線程數(shù)量大于 corePoolSize時,如果某線程空閑時間超過keepAliveTime,線程將被終止,直至線程池中的線程數(shù)目不大于corePoolSize;如果允許為核心池中的線程設(shè)置存活時間,那么核心池中的線程空閑時間超過keepAliveTime,線程也會被終止。
public class Test0007 {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));
for (int i = 1; i <= 6; i++) {
TaskThred t1 = new TaskThred("任務(wù)" + i);
executor.execute(t1);
}
executor.shutdown();
}
}
class TaskThred implements Runnable {
private String taskName;
public TaskThred(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+taskName);
}
}
合理配置線程池
CPU密集
CPU密集的意思是該任務(wù)需要大量的運算,而沒有阻塞,CPU一直全速運行。
CPU密集任務(wù)只有在真正的多核CPU上才可能得到加速(通過多線程),而在單核CPU上,無論你開幾個模擬的多線程,該任務(wù)都不可能得到加速,因為CPU總的運算能力就那些。
IO密集
IO密集型,即該任務(wù)需要大量的IO,即大量的阻塞。在單線程上運行IO密集型的任務(wù)會導(dǎo)致浪費大量的CPU運算能力浪費在等待。所以在IO密集型任務(wù)中使用多線程可以大大的加速程序運行,即時在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。
接著上一篇探討線程池留下的尾巴,如何合理的設(shè)置線程池大小。
要想合理的配置線程池的大小,首先得分析任務(wù)的特性,可以從以下幾個角度分析:
- 任務(wù)的性質(zhì):CPU密集型任務(wù)、IO密集型任務(wù)、混合型任務(wù)。
- 任務(wù)的優(yōu)先級:高、中、低。
- 任務(wù)的執(zhí)行時間:長、中、短。
- 任務(wù)的依賴性:是否依賴其他系統(tǒng)資源,如數(shù)據(jù)庫連接等。
性質(zhì)不同的任務(wù)可以交給不同規(guī)模的線程池執(zhí)行。
對于不同性質(zhì)的任務(wù)來說,CPU密集型任務(wù)應(yīng)配置盡可能小的線程,如配置CPU個數(shù)+1的線程數(shù),IO密集型任務(wù)應(yīng)配置盡可能多的線程,因為IO操作不占用CPU,不要讓CPU閑下來,應(yīng)加大線程數(shù)量,如配置兩倍CPU個數(shù)+1,而對于混合型的任務(wù),如果可以拆分,拆分成IO密集型和CPU密集型分別處理,前提是兩者運行的時間是差不多的,如果處理時間相差很大,則沒必要拆分了。
若任務(wù)對其他系統(tǒng)資源有依賴,如某個任務(wù)依賴數(shù)據(jù)庫的連接返回的結(jié)果,這時候等待的時間越長,則CPU空閑的時間越長,那么線程數(shù)量應(yīng)設(shè)置得越大,才能更好的利用CPU。
當然具體合理線程池值大小,需要結(jié)合系統(tǒng)實際情況,在大量的嘗試下比較才能得出,以上只是前人總結(jié)的規(guī)律。
最佳線程數(shù)目 = ((線程等待時間+線程CPU時間)/線程CPU時間 )* CPU數(shù)目
比如平均每個線程CPU運行時間為0.5s,而線程等待時間(非CPU運行時間,比如IO)為1.5s,CPU核心數(shù)為8,那么根據(jù)上面這個公式估算得到:((0.5+1.5)/0.5)8=32。這個公式進一步轉(zhuǎn)化為:
最佳線程數(shù)目 = (線程等待時間與線程CPU時間之比 + 1) CPU數(shù)目
可以得出一個結(jié)論:
線程等待時間所占比例越高,需要越多線程。線程CPU時間所占比例越高,需要越少線程。
以上公式與之前的CPU和IO密集型任務(wù)設(shè)置線程數(shù)基本吻合。
CPU密集型時,任務(wù)可以少配置線程數(shù),大概和機器的cpu核數(shù)相當,這樣可以使得每個線程都在執(zhí)行任務(wù)
IO密集型時,大部分線程都阻塞,故需要多配置線程數(shù),2*cpu核數(shù)
操作系統(tǒng)之名稱解釋:
某些進程花費了絕大多數(shù)時間在計算上,而其他則在等待I/O上花費了大多是時間,
前者稱為計算密集型(CPU密集型)computer-bound,后者稱為I/O密集型,I/O-bound。