多線程併發 (一) 瞭解 Java 虛擬機 - JVM
多線程併發 (二) 瞭解 Thread
多線程併發 (三) 鎖 synchronized、volatile
多線程併發 (四) 瞭解原子類 AtomicXX 屬性地址偏移量
多線程併發 (五) ReentrantLock 使用和源碼
多線程併發 (六) 瞭解死鎖
多線程併發 (七) 線程池
很高興堅持學到了多線程併發計劃中的最後一個知識點線程池的使用和原理。其實對線程池不陌生,只是簡單的會使用,對於具體的功能實現,一直還是沒有去看,最近乘着多線程併發的學習,來把線程池給記錄下來。
1.線程池引入、優點
如果在一個任務量非常多,但是任務又非常短小,如果我們在處理這種任務時,爲每個任務都創建一個線程,這樣就會創建並且銷燬線程消耗資源、浪費時間。爲了充分發揮線程的利用價值,所以在這種情況下線程池是最好的選擇。
線程池的優點:
- 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
- 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
- 提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。
2.線程池的使用
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(3, 4, 10, TimeUnit.SECONDS,
new LinkedTransferQueue<Runnable>());
for (int i =0;i<20;i++){
final int finalI = i;
try {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100* finalI);
Log.e("wxy","-----" + Thread.currentThread());
} catch (InterruptedException e) {
}
}
});
}catch (Exception e){
Log.e("wxy","--e---");
}
}
通過使用的例子,可以看出要了解線程池主要有兩個入口、1.構造方法、2.execute() 方法。下面會一一介紹。
3.線程池構造方法參數
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
各參數含義:
- corePoolSize:核心線程數量
- maximumPoolSize:最大線程數量
-
keepAliveTime:當線程池中的線程數量超過corePoolSize的時,如果這時沒有新的任務持行,等待keepAliveTime時間之後超過的線程就會銷燬
-
unit:keepAliveTime 時間的單位。有 秒 second 等
-
workQueue:等待隊列,當任務提交時,如果線程池中的線程數量大於等於corePoolSize的時候,把該任務封裝成一個Worker對象放入等待隊列
-
threadFactory:用來創建新線程,默認使用Executors.defaultThreadFactory() 來創建線程,線程具有相同的NORM_PRIORITY優先級並且是非守護線程
-
handler:它是RejectedExecutionHandler類型的變量,表示線程池的飽和策略。如果阻塞隊列滿了並且沒有空閒的線程,這時如果繼續提交任務,就需要採取一種策略處理該任務。線程池提供了4種策略:
1.AbortPolicy:直接拋出異常,這是默認策略;
2.CallerRunsPolicy:用調用者所在的線程來執行任務;
3.DiscardOldestPolicy:丟棄阻塞隊列中靠最前的任務,並執行當前任務;
4.DiscardPolicy:直接丟棄任務;
這些參數就代表線程池的持行策略、線程池大致的持行策略是:
- 有任務提交時先判斷當前線程數量 tCount < corePoolSize 直接創建線程提交任務
- 如果tCount > corePoolSize 再判斷workQueue 是否已經存滿,沒有存滿把當前任務加入等待隊列,注意隊列有有界隊列和無界隊列,是情況而定,減少cup消耗。
- 如果workQueue 已滿再如果 tCount < maximumPoolSize 創建線程持行任務
- 如果 tCount > maximumPoolSize 根據 handler 拒絕策略決定
過程: corePoolSize ---> workQueue ------> maximumPoolSize -----> handler
線程池的持行策略在execute()方法中完全體現, execute代碼就是按照這個思路實現的。看execute方法。
4.execute()方法
public void execute(Runnable command) {
// c 32位 記錄線程池狀態和線程數量
int c = ctl.get();
// 當前線程數量 和 核心數量大小比較
if (workerCountOf(c) < corePoolSize) {
// 小於,持行任務
if (addWorker(command, true))
return;
c = ctl.get();
}
// 否則,提交到 workQueue 隊列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 隊列滿了,判斷最大線程數量,超過參數拒絕策略
else if (!addWorker(command, false))
reject(command);
}
這個流程和三中說的一致,裏面需要特殊關注的是 int c = ctl.get(); 這個值是 32位 記錄線程池狀態和線程數量
5. 線程池狀態和線程數量
// 原子類型
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// Integer.SIZE = 32 整型的位數
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 線程池狀態
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// 得到線程數,也就是後29位的數字。 直接跟CAPACITY做一個與操作即可,
CAPACITY就是的值就 1 << 29 - 1 = 00011111111111111111111111111111。
與操作的話前面3位肯定爲0,相當於直接取後29位的值
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 得到狀態,CAPACITY的非操作得到的二進制位11100000000000000000000000000000,
然後做在一個與操作,相當於直接取前3位的的值
private static int workerCountOf(int c) { return c & CAPACITY; }
// 或操作。相當於更新數量和狀態兩個操作
private static int ctlOf(int rs, int wc) { return rs | wc; }
整個線程池幾乎全是被這個數控制的,所以要想完全瞭解線程池,首先要了解一上代碼的含義。
- 首先,我們知道java中1個整型佔4個字節,也就是32位,所以1個整型有32位。
整型1用二進制表示就是:00000000000000000000000000000001 - 整型-1用二進制表示就是:11111111111111111111111111111111(這個是補碼,不懂的同學可以看下原碼,反碼,補碼的知識)
- 在ThreadPoolExecutor,整型中32位的前3位用來表示線程池狀態,後3位表示線程池中有效的線程數。
// 原子類型 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // Integer.SIZE = 32 整型的位數 private static final int COUNT_BITS = Integer.SIZE - 3;
CONT_BITS 就是29
-
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
線程池容量大小爲 1 << 29 - 1 = 00011111111111111111111111111111(二進制)
-
private static final int RUNNING = -1 << COUNT_BITS;
RUNNING狀態 -1 << 29 = 11111111111111111111111111111111 << 29 = 11100000000000000000000000000000(前3位爲111)。表示-1的二進制 左移 29位,前3位爲111表示線程池狀態
-
private static final int SHUTDOWN = 0 << COUNT_BITS;
SHUTDOWN狀態 0 << 29 = 00000000000000000000000000000000 << 29 = 00000000000000000000000000000000(前3位爲000)
-
private static final int TERMINATED = 3 << COUNT_BITS;
TERMINATED狀態 3 << 29 = 00000000000000000000000000000011 << 29 = 01100000000000000000000000000000(前3位爲011)
摘學於:https://fangjian0423.github.io/2016/03/22/java-threadpool-analysis/
瞭解了線程池的狀態和數量之後,看下addWorker方法
6.addWorker方法
addWorker方法有兩個參數,1.firstTask參數 用於指定新增的線程執行的第一個任務,2.core參數爲true表示在新增線程時會判斷當前活動線程數是否少於corePoolSize,false表示新增線程前需要判斷當前活動線程數是否少於maximumPoolSize
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
// 獲取運行狀態
int rs = runStateOf(c);
/*
* 這個if判斷
* 如果rs >= SHUTDOWN,則表示此時不再接收新任務;
* 接着判斷以下3個條件,只要有1個不滿足,則返回false:
* 1. rs == SHUTDOWN,這時表示關閉狀態,不再接受新提交的任務,但卻可以繼續處理阻塞隊列中已保存的任務
* 2. firsTask爲空
* 3. 阻塞隊列不爲空
*
* 首先考慮rs == SHUTDOWN的情況
* 這種情況下不會接受新提交的任務,所以在firstTask不爲空的時候會返回false;
* 然後,如果firstTask爲空,並且workQueue也爲空,則返回false,
* 因爲隊列中已經沒有任務了,不需要再添加線程了
*/
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
// 獲取線程數
int wc = workerCountOf(c);
// 如果wc超過CAPACITY,也就是ctl的低29位的最大值(二進制是29個1),返回false;
// 這裏的core是addWorker方法的第二個參數,如果爲true表示根據corePoolSize來比較,
// 如果爲false則根據maximumPoolSize來比較。
//
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 嘗試增加workerCount,如果成功,則跳出第一個for循環
if (compareAndIncrementWorkerCount(c))
break retry;
// 如果增加workerCount失敗,則重新獲取ctl的值
c = ctl.get(); // Re-read ctl
// 如果當前的運行狀態不等於rs,說明狀態已被改變,返回第一個for循環繼續執行
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 根據firstTask來創建Worker對象
w = new Worker(firstTask);
// 每一個Worker對象都會創建一個線程
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
// rs < SHUTDOWN表示是RUNNING狀態;
// 如果rs是RUNNING狀態或者rs是SHUTDOWN狀態並且firstTask爲null,向線程池中添加線程。
// 因爲在SHUTDOWN時不會在添加新的任務,但還是會執行workQueue中的任務
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// workers是一個HashSet
workers.add(w);
int s = workers.size();
// largestPoolSize記錄着線程池中出現過的最大線程數量
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 啓動線程
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
在這個方法中會啓動線程,調用start方法,啓動之後就調用Worker的run方法,所以先看下Worker類
7.Worker類
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
final Thread thread;
Runnable firstTask;
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1); // 修改狀態
this.firstTask = firstTask;
//傳入自己所以線程start後會調用下面的 run 方法
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
..............................................
}
Worker類是一個繼承AQS,實現Runable的類,裏面封裝了thread 和 runable。
Worker繼承了AQS,使用AQS來實現獨佔鎖的功能。爲什麼不使用ReentrantLock來實現呢?可以看到tryAcquire方法,它是不允許重入的,而ReentrantLock是允許重入的:
- lock方法一旦獲取了獨佔鎖,表示當前線程正在執行任務中;
- 如果正在執行任務,則不應該中斷線程;
- 如果該線程現在不是獨佔鎖的狀態,也就是空閒的狀態,說明它沒有在處理任務,這時可以對該線程進行中斷;
- 線程池在執行shutdown方法或tryTerminate方法時會調用interruptIdleWorkers方法來中斷空閒的線程,interruptIdleWorkers方法會使用tryLock方法來判斷線程池中的線程是否是空閒狀態;
- 之所以設置爲不可重入,是因爲我們不希望任務在調用像setCorePoolSize這樣的線程池控制方法時重新獲取鎖。如果使用ReentrantLock,它是可重入的,這樣如果在任務中調用瞭如setCorePoolSize這類線程池控制的方法,會中斷正在運行的線程。
所以,Worker繼承自AQS,用於判斷線程是否空閒以及是否可以被中斷。
8.runWorker方法
在addWorker中調用start之後就會調用Worker對象的 run方法,然後就調用了 runWorker 方法
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// 獲取第一個任務
Runnable task = w.firstTask;
w.firstTask = null;
// 允許中斷
w.unlock(); // allow interrupts
// 是否因爲異常退出循環
boolean completedAbruptly = true;
try {
// 如果task爲空,則通過getTask來獲取任務
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
runWorker方法的執行過程:
- while循環不斷地通過getTask()方法獲取任務;
- getTask()方法從阻塞隊列中取任務;
- 如果線程池正在停止,那麼要保證當前線程是中斷狀態,否則要保證當前線程不是中斷狀態;
- 調用
task.run()
執行任務; - 如果task爲null則跳出循環,執行processWorkerExit()方法;
- runWorker方法執行完畢,也代表着Worker中的run方法執行完畢,銷燬線程。
9.getTask方法
private Runnable getTask() {
// timeOut變量的值表示上次從阻塞隊列中取任務時是否超時
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
/*
* 如果線程池狀態rs >= SHUTDOWN,也就是非RUNNING狀態,再進行以下判斷:
* 1. rs >= STOP,線程池是否正在stop;
* 2. 阻塞隊列是否爲空。
* 如果以上條件滿足,則將workerCount減1並返回null。
* 因爲如果當前線程池狀態的值是SHUTDOWN或以上時,不允許再向阻塞隊列中添加任務。
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
// timed變量用於判斷是否需要進行超時控制。
// allowCoreThreadTimeOut默認是false,也就是核心線程不允許進行超時;
// wc > corePoolSize,表示當前線程池中的線程數量大於核心線程數量;
// 對於超過核心線程數量的這些線程,需要進行超時控制
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
/*
* wc > maximumPoolSize的情況是因爲可能在此方法執行階段同時執行了setMaximumPoolSize方法;
* timed && timedOut 如果爲true,表示當前操作需要進行超時控制,並且上次從阻塞隊列中獲取任務發生了超時
* 接下來判斷,如果有效線程數量大於1,或者阻塞隊列是空的,那麼嘗試將workerCount減1;
* 如果減1失敗,則返回重試。
* 如果wc == 1時,也就說明當前線程是線程池中唯一的一個線程了。
*/
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
/*
* 根據timed來判斷,如果爲true,則通過阻塞隊列的poll方法進行超時控制,如果在keepAliveTime時間內沒有獲取到任務,則返回null;
* 否則通過take方法,如果這時隊列爲空,則take方法會阻塞直到隊列不爲空。
*
*/
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
// 如果 r == null,說明已經超時,timedOut設置爲true
timedOut = true;
} catch (InterruptedException retry) {
// 如果獲取任務時當前線程發生了中斷,則設置timedOut爲false並返回循環重試
timedOut = false;
}
}
}
10.tryTerminate方法
final void tryTerminate() {
for (;;) {
int c = ctl.get();
/*
* 當前線程池的狀態爲以下幾種情況時,直接返回:
* 1. RUNNING,因爲還在運行中,不能停止;
* 2. TIDYING或TERMINATED,因爲線程池中已經沒有正在運行的線程了;
* 3. SHUTDOWN並且等待隊列非空,這時要執行完workQueue中的task;
*/
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
// 如果線程數量不爲0,則中斷一個空閒的工作線程,並返回
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 這裏嘗試設置狀態爲TIDYING,如果設置成功,則調用terminated方法
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
// terminated方法默認什麼都不做,留給子類實現
terminated();
} finally {
// 設置狀態爲TERMINATED
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
嘗試終止線程池。
整個多線程併發涵蓋的知識點有了一個總結,從系列一開始到七,大致講述了整個併發的知識網。對併發到這裏就先告一段落。
總的來說也是站在前人文章/書籍的基礎上進行學習的,在知識點整理的過程中如有侵犯,請告訴我。因爲總的來說是對其他開發者文章/書籍的整理學習。看了大量的文章/書籍,因爲有的文章會不夠完整,就把看到的學到的有關連的,一點一點的總結。
摘學於:
https://fangjian0423.github.io/2016/03/22/java-threadpool-analysis/
https://www.jianshu.com/p/d2729853c4da