開篇閒扯
這應該是短時間內最後一篇原創多執行緒的文章了,不是因為別的,就是因為起名字有點詞窮了,也不知道UC編輯部啥時候能有我一個位置。
其實這6篇文章僅僅是多執行緒的冰山一小角,不論是面試還是實際工作開發,這些都是不夠的。還是要多看書本上的知識,看部落格得到的知識點都是盲人摸象,不成體系,這是最可怕的。如果把多執行緒比作一塊拼圖的話,那麼你看的每一篇(包括我的文章)部落格都僅僅是這塊拼圖中的零散幾片,需要很長時間才能得到這個完整的多執行緒拼圖。而當你集齊了多執行緒拼圖的時候又會發現,原來多執行緒也不過是整個Java生態裡的一小塊,而Java生態也僅僅是軟體開發行業中的產業鏈之一。
讀專業書籍的好處在於能夠快速建立相關知識的體系,少走彎路,降低重複收集知識拼圖的可能性。氣氛烘托到這兒了,推薦幾本多執行緒的書給大家吧。第一本《Java併發執行緒入門到精通》作者是張振華,屬於入門到精通類書本。另一本則是Doug Lea參與編寫的《Java併發程式設計實戰》,這本書值得精讀,讀完你會覺得腳底生風,風生水起,起伏不定...還有就是我不是賣書的,如果你不想花(想)錢(白)買(嫖),可以公眾號私信我,免費分享PDF,僅供學習交流昂,我們也免責一下。
扯多了,那麼本篇主要內容就是執行緒池,包括SingleThreadPool、FixedThreadPool、ScheduledThreadPool、CacheThreadPool和WorkStealingThreadPool等不同水池子,其中前4中比較常見,最後一種是在1.8及以後新增的執行緒池。
執行緒池概述
在開發過程中,我們都知道使用多執行緒來提升系統的吞吐量,但是無限制的建立執行緒反而會給系統帶來負擔,記憶體分配不足,CPU超負荷等等,而且過多的執行緒也會導致頻繁的執行緒切換,前面有寫過:頻繁的上下文切換會導致嚴重的效能損耗。
以我們常見的HotSport虛擬機器為例,它的執行緒模型規定Java建立的執行緒是和計算機核心執行緒是一對一的,那麼Java執行緒的建立與銷燬就對應著核心執行緒的建立與銷燬,而執行緒建立與銷燬設計到記憶體空間申請、分配等動作,對資源的消耗可想而知。因此,執行緒池的出現就為讓已知任務量的系統中保持著一定數量的工作執行緒,提升執行緒複用,控制執行緒數量,降低執行緒建立銷燬的頻率。下面用一張圖展示執行緒池的工作原理:
四種拒絕策略
策略名 | 解釋 |
---|---|
AbortPolicy | 丟棄任務並丟擲RejectedExecutionException,預設該方式。 |
DiscardPolicy | 丟棄任務,但是不丟擲異常 |
DiscardOldestPolicy | 丟棄workQueue中最前面的任務,然後重新嘗試執行任務 |
CallerRunsPolicy | 由呼叫執行緒自己處理該任務 |
五種執行緒池狀態
狀態名 | 解釋 |
---|---|
running | 該狀態的執行緒池能夠接受新任務,並對新新增的任務進行處理 |
shutdown | 該狀態的執行緒池不再接受新任務,但是會把阻塞佇列中的任務全部執行完 |
stop | 該狀態的執行緒池不再接收新任務,不處理阻塞佇列的任務,並且中斷正在處理的任務 |
tidying | 當所有的任務已終止,ctl記錄的"任務數量"為0,執行緒池會變為TIDYING狀態 |
terminated | 執行緒池徹底終止的狀態 |
通過原始碼看一下執行緒池狀態在程式碼中是如何體現的:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//表示一個int型佔用32位,減去3位表示29位
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//將-1左移29位後變成111
private static final int RUNNING = -1 << COUNT_BITS;
//將0左移29位後是000
private static final int SHUTDOWN = 0 << COUNT_BITS;
//將0左移29位後是001
private static final int STOP = 1 << COUNT_BITS;
//將0左移29位後是010
private static final int TIDYING = 2 << COUNT_BITS;
//將0左移29位後是011
private static final int TERMINATED = 3 << COUNT_BITS;
通過原始碼看到,通過ctl的高3位記錄了執行緒池的執行狀態,而低29位則記錄了執行緒池中的任務數量。
六個核心引數
引數名 | 引數說明 |
---|---|
corePoolSize(核心執行緒數) | 核心執行緒數量,執行緒池維護執行緒的最少數量 |
workQueue(任務等待佇列) | 儲存等待執行任務的阻塞佇列 |
maximumPoolSize(執行緒池執行緒數最大值) | 執行緒池所允許的最大執行緒個數。當佇列滿了,且已建立的執行緒數小於maximumPoolSize,則執行緒池會建立新的執行緒來執行任務 |
keepAliveTime(執行緒存活時間) | 當執行緒池中執行緒數大於核心執行緒數時,執行緒的空閒時間如果超過執行緒存活時間,那麼這個執行緒就會被銷燬,直到執行緒池中的執行緒數小於等於核心執行緒數 |
threadFactory(執行緒工廠) | 用於建立新執行緒。threadFactory建立的執行緒也是採用new Thread()方式,threadFactory建立的執行緒名都具有統一的風格:pool-m-thread-n(m為執行緒池的編號,n為執行緒池內的執行緒編號)。 |
handler(執行緒飽和策略) | 當執行緒池和佇列都滿了,再加入執行緒會執行此策略。 |
七種阻塞佇列
佇列名 | 資料結構 | 解釋 |
---|---|---|
ArrayBlockingQueue | ArrayList | 由陣列結構組成的有界阻塞佇列 |
LinkedBlockingQueue | LinkList | 由連結串列結構組成的無界阻塞佇列 |
PriorityBlockingQueue | heap | 支援優先順序排序的無界阻塞佇列 |
DealyQueue | heap | 使用優先順序佇列實現的無界阻塞佇列 |
SynchronousQueue | 無 | 不儲存元素的阻塞佇列 |
LinkedTransferQueue | heap | 由連結串列結構組成的無界阻塞佇列 |
LinkedBlockingDeque | heap | 由連結串列結構組成的雙向阻塞佇列 |
阻塞佇列這塊東西特別多,在併發包裡的地位也很高,就這七個佇列如果想寫的很詳細夠寫七篇文章的,這裡就不仔細說了,因為我也還沒研究他們的實現,暫時也只是略有了解....記在小本本上了,我會補上的。
五種常見執行緒池
SingleThreadPool
先看一下它的構造方法:
// 重點在阻塞佇列上
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
把它上面的註釋翻譯過來就是“建立一個使用單個工作執行緒的執行器,同時它的佇列是無界的。(但是請注意,如果這個單個執行緒在關閉之前的執行過程中意外終止,則在需要執行後續任務時,將替換一個新的執行緒。)任務被保證按順序執行,並且在任何給定的時間內不會有多個任務處於活動狀態”還特別強調了:Unlike the otherwise equivalent newFixedThreadPool(1),有興趣可以研究一下原始碼,但其實沒必要。
演示一下基本用法:
/**
* FileName: SingleThreadPool
* Author: RollerRunning
* Date: 2020/12/14 7:35 PM
* Description: 但執行緒的執行緒池
*/
public class SingleThreadPool {
public static ExecutorService singleThreadExecutor;
static {
singleThreadExecutor = Executors.newSingleThreadExecutor();
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
final int temp = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName(String.valueOf(temp));
System.out.println("執行緒 "+Thread.currentThread().getName()+" 開始執行...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
singleThreadExecutor.shutdown();
System.out.println("===========Main Thread Over!=========");
}
}
大家可以嘗試執行一下,能夠看到所有執行緒都是順序執行的,SingleThreadExecutor是一個單執行緒的執行緒池,如果當前執行緒意外終止,執行緒池會建立一個新執行緒繼續執行任務,以保證任務能夠完成。
不知道你們通過這個例子有沒有發現問題,其實這麼建立執行緒池,是有把服務搞死的風險,通過原始碼可以看到構造方法中用到的任務等待佇列是LinkedBlockingQueue,理論上說它是一個無線佇列,最大值為Integer.MAX_VALUE,就有OOM的風險。因此,在使用時,如果沒有特殊需求,還是使用ThreadPoolExecutor進行建立執行緒池,可以自定義執行緒池的任意引數。
FixedThreadPool
先看一下它的構造方法:
// 指定執行緒數
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 執行執行緒數和執行緒工廠類
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
基礎用法同上面的類似,只是初始化執行緒池用到的API不同
public class FixedThreadPool {
public static ExecutorService fixedThreadPool;
static {
//初始化固定核心執行緒數的執行緒池
fixedThreadPool = Executors.newFixedThreadPool(5);
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 20; i++) {
final int temp = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName(String.valueOf(temp));
try {
System.out.println("執行緒 " + Thread.currentThread().getName() + " 開始...");
Thread.sleep(1000);
System.out.println("執行緒 " + Thread.currentThread().getName() + " 結束...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
System.out.println("*********************");
fixedThreadPool.shutdown();
System.out.println("===========Main Thread Over!=========");
}
}
ScheduledThreadPool
建立一個週期執行緒池,支援定時週期性執行執行緒池中的任務。
上構造:
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
可以看出不同於其他執行緒池的點是使用了延遲工作佇列,在原始碼中也增加了兩個定時執行的方法:
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit) {}
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit) {}
簡單舉個例子:
public class ScheduledThreadPool {
public static ScheduledExecutorService scheduledThreadPool;
static {
//初始化固定核心執行緒數的執行緒池
scheduledThreadPool = Executors.newScheduledThreadPool(5);
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 5; i++) {
final int temp = i;
scheduledThreadPool.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName(String.valueOf(temp));
try {
System.out.println("執行緒 " + Thread.currentThread().getName() + " 每隔10秒執行一次...");
Thread.sleep(500);// 1
System.out.println("執行緒 " + Thread.currentThread().getName() + " 結束...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 3, 10, TimeUnit.SECONDS);
}
System.out.println("===========Main Thread Over!=========");
}
}
這裡演示了一種用法,即:執行緒池啟動3S以後開始執行任務,每隔10S將這5個任務都執行一遍。如果將程式碼 1 位置處的500ms改成11s呢?這時候設定的每隔10s週期性執行就會失效,可以複製到自己本地執行看看現象,因此使用這類執行緒池還是要注意每個任務的處理耗時,合理評估間隔時間。感興趣的還可以自己嘗試使用scheduleAtFixedRate()的方法以及其他的使用方式。
再分享一個小Tip:可以考慮多執行緒池巢狀使用,合理利用Executors下預設執行緒池的特性。
CacheThreadPool
帶有快取的執行緒池,如果執行緒池長度超過處理需要,執行緒池可以靈活回收空閒執行緒,若無可回收執行緒,就建立新執行緒。
構造方法:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//可自定義執行緒工廠類構造
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
通過構造器可以看出,可以自定義執行緒的工廠類,而且跟其他幾個預設的執行緒池區別在於阻塞佇列的不同,也有個同樣致命的缺點:Integer.MAX_VALUE,存在OOM的風險。
簡單示例:
public class CachedThreadPool {
public static ExecutorService cachedThreadPool;
static {
//初始化帶有快取的執行緒池
cachedThreadPool = Executors.newCachedThreadPool();
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 5; i++) {
final int temp = i;
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName(String.valueOf(temp));
try {
System.out.println("執行緒 " + Thread.currentThread().getName() + " 開始...");
Thread.sleep(2000);
System.out.println("執行緒 " + Thread.currentThread().getName() + " 結束...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
System.out.println("===========Main Thread Over!=========");
}
}
到這裡一共介紹了4種執行緒池的構造和基礎用法,但是這些預設構造在極端情況下都存在風險,這種使用Executors返回的執行緒池物件風險如下:
1.FixedThreadPool和SingleThreadPool:允許的請求佇列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,導致OOM
2.CachedThreadPool和ScheduledThreadPool:允許的建立執行緒數量為 Integer.MAX_VALUE,可能會建立大量的執行緒,導致 OOM。
還是那句話,沒有特殊需求,就老老實實的按照阿里規約裡的來寫:
new ThreadPoolExecutor(“核心執行緒數”, “最大執行緒數”, “空閒執行緒存活時間”, “時間單位”, “阻塞佇列型別”);
所有的引數都可以自定義,不過也不能設計的太灑脫,還是要根據業務場景合理設定,那怎麼叫合理呢?簡單!分兩個場景介紹:
1.CPU(計算)密集型
假設CPU核心數為N,那麼一般可以將核心執行緒數設定為N+1個,可以充分利用CPU的資源,且降低了CPU單個核心的上下文切換的頻率。至於為什麼要比CPU多設定一個執行緒,是因為防止某個時間點執行緒意外終止,此時多設定的這個執行緒就可以頂上了。
上demo證明一下這個理論對不對,不對的話,我就去找一下Doug Lea先生,跟他battle一下
public class ThreadPoolTest {
// 初始一個執行緒池
private static ThreadPoolExecutor threadPool;
//計算執行緒池執行總時長
private static Vector<Long> threadPoolRunTime;
//單個執行緒執行市場
private static Vector<Long> singleThreadRunTime;
static {
// 用於獲取當前硬體裝置的CPU核心數
int coreNum = Runtime.getRuntime().availableProcessors();
System.out.println("當前裝置CPU核心數:" + coreNum);
// 這裡是重點,通過修改核心執行緒數和最大執行緒數,來觀察任務執行耗時變化,即可證明前面闡述的理論
threadPool = new ThreadPoolExecutor(coreNum + 1, coreNum + 1, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1000), new ThreadPoolExecutor.DiscardPolicy());
threadPoolRunTime = new Vector<Long>();
singleThreadRunTime = new Vector<Long>();
}
public static void main(String[] args) throws Exception {
List<Future<?>> futureTaskList = new ArrayList<Future<?>>();
//執行總次數可以不修改
int testNum = 100;
for (int i = 0; i < testNum; i++) {
//提交一個計算密集型任務,並計算單個任務總耗時
Future<?> future = threadPool.submit(new TestCPU(singleThreadRunTime, threadPoolRunTime));
//提交一個IO密集型任務,並計算單個任務總耗時
//Future<?> future = threadPool.submit(new TestIO(singleThreadRunTime, threadPoolRunTime));
futureTaskList.add(future);
}
for (Future<?> future : futureTaskList) {
//獲取執行緒執行結果
future.get(testNum, TimeUnit.SECONDS);
}
System.out.println("執行總耗時: " + getTime(threadPoolRunTime) / threadPoolRunTime.size() + " ms");
System.out.println("單個執行緒平均耗時: " + getTime(singleThreadRunTime) / singleThreadRunTime.size() + " ms");
threadPool.shutdown();
}
public static Long getTime(Vector<Long> list) {
long time = 0;
for (int i = 0; i < list.size(); i++) {
time = list.get(i) + time;
}
return time;
}
}
/**
* FileName: TestCPU
* Author: RollerRunning
* Date: 2020/12/16 9:08 PM
* Description: 計算密集型任務
*/
public class TestCPU implements Runnable {
private static List<Long> threadPoolRunTime;
private static List<Long> singleThreadRunTime;
private long startTime = 0;
public TestCPU(List<Long> singleThreadRunTime, List<Long> threadPoolRunTime) {
startTime = System.currentTimeMillis();
this.singleThreadRunTime = singleThreadRunTime;
this.threadPoolRunTime = threadPoolRunTime;
}
@Override
public void run() {
long start = System.currentTimeMillis();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//統計1~100000之間素數的總數
countPrimes(1, 100000);
long endTime = System.currentTimeMillis();
long threadPoolTime = endTime - startTime;
long threadTime = endTime - start;
threadPoolRunTime.add(threadPoolTime);
singleThreadRunTime.add(threadTime);
System.out.println("當前線" + Thread.currentThread().getName() + "程耗時:" + (endTime - start) + " ms");
}
/**
* 判斷是否為素數
*/
public boolean isPrime(final int num) {
if (num <= 1) {
return false;
}
for (int i = 2; i <= Math.sqrt(num); i++) {
if (num % i == 0) {
return false;
}
}
return true;
}
/**
* 計算素數
*/
public int countPrimes(final int startNum, final int endNum) {
int count = 0;
for (int i = startNum; i <= endNum; i++) {
if (isPrime(i)) {
count++;
}
}
return count;
}
}
可以複製到自己本地跑一下這個測試類,根據自己本地CPU核心數設定不同的執行緒池核心數,能夠發現執行結果有很大的不同,而效率最高的時候,就是當執行緒池中執行緒數與CPU核心數差不多相等的時候,會有差異,但不會太大。
2.I/O密集型
這類程式,由於系統大多數時間都是在處理IO互動,在處理IO的時間段內,是不會佔用CPU時間片的,因此CPU有能力處理更多的執行緒。假設W:執行緒等待IO資源的時間;C:執行緒執行時間;P:目標CPU使用率。N:CPU數量,那麼相對最優執行緒池大小Core = N * P * (1 + W/C)
還是建立一個IO密集型的任務,然後批量提交到我們的執行緒池中做測試:
/**
* FileName: TestIO
* Author: RollerRunning
* Date: 2020/12/16 9:39 PM
* Description: IO密集型任務
*/
public class TestIO implements Runnable {
private static List<Long> threadPoolRunTime;
private static List<Long> singleThreadRunTime;
private long startTime = 0;
public TestIO(Vector<Long> singleThreadRunTime, Vector<Long> threadPoolRunTime) {
startTime = System.currentTimeMillis();
this.singleThreadRunTime = singleThreadRunTime;
this.threadPoolRunTime = threadPoolRunTime;
}
@Override
public void run() {
long start = System.currentTimeMillis();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 執行IO操作
readFile();
} catch (IOException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
threadPoolRunTime.add(end - startTime);
singleThreadRunTime.add(end - start);
System.out.println("當前執行緒" + Thread.currentThread().getName() + "耗時:" + (end - start) + " ms");
}
/**
* IO操作,讀取一個本地檔案
*/
public void readFile() throws IOException {
//自己隨便建立一個txt檔案用來測試
File sourceFile = new File("/Users/RollerRunning/Documents/test/IO.txt");
BufferedReader input = new BufferedReader(new FileReader(sourceFile));
//按行讀取
String line = null;
while ((line = input.readLine()) != null) {
System.out.println(line);
}
input.close();
}
}
還是百看不如一次自己執行,拷貝一下,自己執行感受一下。這些demo書裡都能找到,也可以百度一下其他的部落格寫的一些測試案例,都大差不差。
WorkStealingThreadPool
還是先來看看這個執行緒池的構造方法:
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
這個執行緒池是JDK1.8以後才新增的基於ForkJoinPool擴充套件來的,通過構造方法就能看出來,在它的內部是通過new ForkJoinPool()來實現的,而前面四種是通過new ThreadPoolExecutor()再搭配不同的阻塞佇列實現的,更細節的東西就不多說了,不是一兩篇能寫完的。
那麼這個新增的執行緒池有啥優點呢?優點就是它內部執行緒會steal work啊,小時候有個梗就是吃零食比誰吃得快,誰先吃完可以搶別人的吃,這個執行緒池也是這個思想,誰的任務執行完了,就可以去幫助其他執行緒執行任務了,不過這麼說不夠準確,先看一張圖,然後再詳細解釋
圖一是前四種執行緒池的一個大概的任務分配模型,而圖二則是WorkStealingThreadPool執行緒池的任務分配模式,每一個執行緒都有一個自己的任務等待佇列,也就是自己的零食,當自己的任務執行完了,允許從其他執行緒的任務佇列中獲取任務並協助執行。這樣就可以提高執行緒的可用性,提升執行緒池整體效率。
最後來個簡單的示例:
public class WorkStealThreadPool {
public static ExecutorService workStealThreadPool;
static {
//初始化無鎖執行緒池
workStealThreadPool = Executors.newWorkStealingPool();
}
public static void main(String[] args) throws Exception {
int core = Runtime.getRuntime().availableProcessors();
System.out.println("當前裝置CPU核心數為:" + core);
for (int i = 0; i < 50; i++) {
FutureTask<?> futureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() {
try {
System.out.println("執行緒 " + Thread.currentThread().getName() + " 開始...");
Thread.sleep(1000);
System.out.println("執行緒 " + Thread.currentThread().getName() + " 結束...");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "當前執行緒名稱為:"+Thread.currentThread().getName();
}
});
workStealThreadPool.submit(new Thread(futureTask));
//System.out.println(futureTask.get());
}
System.out.println("===========Main Thread Over!=========");
}
}
更多文章請微信搜尋Java棧點公眾號!