線程池
什麼是線程池
Java中的線程池是運用場景最多的併發框架,幾乎所有需要異步或併發執行任務的程序都可以使用線程池。在開發過程中,合理地使用線程池能夠帶來許多好處。
- 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
- 提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
- 提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用
線程池作用
線程池是爲突然大量爆發的線程設計的,通過有限的幾個固定線程爲大量的操作服務,減少了創建和銷燬線程所需的時間,從而提高效率。
如果一個線程所需要執行的時間非常長的話,就沒必要用線程池了(不是不能作長時間操作,而是不宜。本來降低線程創建和銷燬,結果你那麼久我還不好控制還不如直接創建線程),況且我們還不能控制線程池中線程的開始、掛起、和中止。
如何實現構建線程池
什麼是ThreadPoolExecutor
ThreadPoolExecutor就是線程池
ThreadPoolExecutor其實也是JAVA的一個類,通過Executor工廠類的方法,通過傳入不同的參數,就可以構造出適用於不同應用場景下的ThreadPoolExecutor(線程池)
構造參數圖:
構造參數參數介紹:
corePoolSize 核心線程數量
maximumPoolSize 最大線程數量
keepAliveTime 線程保持時間,N個時間單位
unit 時間單位(比如秒,分)
workQueue 阻塞隊列
threadFactory 線程工廠
handler 線程池拒絕策略
什麼是Executor
Executor框架實現的就是線程池的功能。
Executors工廠類中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其實也只是ThreadPoolExecutor的構造函數參數不同而已。通過傳入不同的參數,就可以構造出適用於不同應用場景下的線程池,
Executor工廠類如何創建線程池圖:
線程池四種創建方式
Java通過Executors(jdk1.5併發包)提供四種線程池,分別爲:
- newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
- newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
- newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
- newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
代碼演示構建線程池的四種方式
newCachedThreadPool
特點:newCachedThreadPool創建一個可緩存線程池,如果當前線程池的長度超過了處理的需要時,它可以靈活的回收空閒的線程,當需要增加時, 它可以靈活的添加新的線程,而不會對池的長度作任何限制
缺點:他雖然可以無線的新建線程,但是容易造成堆外內存溢出,因爲它的最大值是在初始化的時候設置爲 Integer.MAX_VALUE,一般來說機器都沒那麼大內存給它不斷使用。當然知道可能出問題的點,就可以去重寫一個方法限制一下這個最大值
總結:線程池爲無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的線程,而不用每次新建線程。
代碼示例:
package com.lijie;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestNewCachedThreadPool {
public static void main(String[] args) {
// 創建無限大小線程池,由jvm自動回收
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int temp = i;
newCachedThreadPool.execute(new Runnable() {
public void run() {
try {
Thread.sleep(100);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + ",i==" + temp);
}
});
}
}
}
newFixedThreadPool
特點:創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。定長線程池的大小最好根據系統資源進行設置。
缺點:線程數量是固定的,但是阻塞隊列是無界隊列。如果有很多請求積壓,阻塞隊列越來越長,容易導致OOM(超出內存空間)
總結:請求的擠壓一定要和分配的線程池大小匹配,定線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()
Runtime.getRuntime().availableProcessors()方法是查看電腦CPU核心數量)
代碼示例:
package com.lijie;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestNewFixedThreadPool {
public static void main(String[] args) {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int temp = i;
newFixedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + ",i==" + temp);
}
});
}
}
}
newScheduledThreadPool
特點:創建一個固定長度的線程池,而且支持定時的以及週期性的任務執行,類似於Timer(Timer是Java的一個定時器類)
缺點:由於所有任務都是由同一個線程來調度,因此所有任務都是串行執行的,同一時間只能有一個任務在執行,前一個任務的延遲或異常都將會影響到之後的任務(比如:一個任務出錯,以後的任務都無法繼續)。
代碼示例:
package com.lijie;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class TestNewScheduledThreadPool {
public static void main(String[] args) {
//定義線程池大小爲3
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
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秒執行。
}
}
}
newSingleThreadExecutor
特點:創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它,他必須保證前一項任務執行完畢才能執行後一項。保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
缺點:缺點的話,很明顯,他是單線程的,高併發業務下有點無力
總結:保證所有任務按照指定順序執行的,如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它
代碼示例:
package com.lijie;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestNewSingleThreadExecutor {
public static void main(String[] args) {
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
newSingleThreadExecutor.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + " index:" + index);
try {
Thread.sleep(200);
} catch (Exception e) {
}
}
});
}
}
}
自定義線程線程池
先看ThreadPoolExecutor(線程池)這個類的構造參數
構造參數參數介紹:
corePoolSize 核心線程數量
maximumPoolSize 最大線程數量
keepAliveTime 線程保持時間,N個時間單位
unit 時間單位(比如秒,分)
workQueue 阻塞隊列
threadFactory 線程工廠
handler 線程池拒絕策略
代碼示例:
package com.lijie;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Test001 {
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("任務" + i);
//executor.execute(t1);是執行線程方法
executor.execute(t1);
}
//executor.shutdown()不再接受新的任務,並且等待之前提交的任務都執行完再關閉,阻塞隊列中的任務不會再執行。
executor.shutdown();
}
}
class TaskThred implements Runnable {
private String taskName;
public TaskThred(String taskName) {
this.taskName = taskName;
}
public void run() {
System.out.println(Thread.currentThread().getName() + taskName);
}
}
線程池原理剖析
提交一個任務到線程池中,線程池的處理流程如下:
- 判斷線程池裏的核心線程是否都在執行任務,如果不是(核心線程空閒或者還有核心線程沒有被創建)則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則進入下個流程。
- 線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列裏。如果工作隊列滿了,則進入下個流程。
- 判斷線程池裏的線程是否都處於工作狀態,如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。
如何合理分配線程池大小
什麼是CPU密集
CPU密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速運行。
CPU密集任務只有在真正的多核CPU上纔可能得到加速(通過多線程),而在單核CPU上,無論你開幾個模擬的多線程,該任務都不可能得到加速,因爲CPU總的運算能力就那樣。
什麼是IO密集
IO密集型,即該任務需要大量的IO,即大量的阻塞。在單線程上運行IO密集型的任務會導致浪費大量的CPU運算能力浪費在等待。所以在IO密集型任務中使用多線程可以大大的加速程序運行,即時在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。
分配CPU和IO密集:
-
CPU密集型時,任務可以少配置線程數,大概和機器的cpu核數相當,這樣可以使得每個線程都在執行任務
-
IO密集型時,大部分線程都阻塞,故需要多配置線程數,2*cpu核數
合理計算
要想合理的配置線程池的大小,首先得分析任務的特性,可以從以下幾個角度分析:
- 任務的性質:CPU密集型任務、IO密集型任務、混合型任務。
- 任務的優先級:高、中、低。
- 任務的執行時間:長、中、短。
- 任務的依賴性:是否依賴其他系統資源,如數據庫連接等。
性質不同的任務可以交給不同規模的線程池執行。
-
CPU密集型任務應配置儘可能小的線程,如配置CPU個數+1的線程數
-
IO密集型任務應配置儘可能多的線程,因爲IO操作不佔用CPU,不要讓CPU閒下來,應加大線程數量
可以得出一個結論:
線程等待時間比CPU執行時間比例越高,需要越多線程。
線程CPU執行時間比等待時間比例越高,需要越少線程。