知識梳理系列之一——多線程
本文簡明的梳理下多線程基本知識
知識梳理系列之一
幾個問題
1. 爲什麼要有多線程
總結:
在做複雜計算時(比如在CPU中做複雜的算術或者邏輯計算、GPU中柵格化圖像數據等)或者遠程(網絡/磁盤文件流等)數據獲取時,一般需要耗費大量時間,如果由主線程執行這些操作將會造成主線程阻塞,這種情況是不應出現的。因此,需要創建工作線程並在其中執行。
2. 有哪些方式實現在工作線程中執行操作
常用的包括創建Thread/異步任務AsyncTask/HandlerThread和IntentService/線程池等幾類
2.1 Thread/Runnable/FutrueTask 知識及其優缺點
Thread 最普通的使用方式:
// 1. Thread構造時,會調用Thread init()方法初始化, 對線程名、Group、TID、Target(Runnable)、線程優先級、
// deamon(布爾值是否是守護線程)、stackSize(線程棧內存大小)等進行賦值,Thread本身實現了Runnable接口,run()方法執行的就是target.run()
Thread t = new Thread(new Runnable() {
//匿名內部類實現Runnable,也可以自定義集成或聲明爲局部或成員變量
@Override
public void run() {
// do something...
}
});
// 2. 必須調用start(),纔是真正啓動了一個線程,
// 因爲start()同步方法中調用了native方法nativeCreate(java.lang.Thread, long, boolean) 才真正創建了線程
t.start();
在可執行任務接口(Runnable)中實現要執行的業務,構造Java Thread類的實例,通過Thread.start()方法來啓動這個線程並最終執行Runnable.run()方法。
- Runnable與Callable接口:Runnable接口的run方法是void的,沒有返回值,當我們需要用線程執行一個任務並且得到返回值時,就可以使用Callable接口,Callable接口支持一個泛型在調用call方法後返回這個泛型的結果,但是Thread是不可以直接使用的,target是Runnable型。於是產生了一個FutureTask的類型:
Callable在FutureTask構造時初始化,run()方法中,觸發callable.call()方法,得到result調用set(V)方法保存,並觸發finishCompletion()、done()(一個空實現方法)方法,可重寫done()方法,通過get() (調用report(int))獲取結果;
即: run() --> callable.call() --> set(result) --> finishCompletion() --> done() --> 重寫done() 中調用get() --> report() 獲取任務結果。
FutureTask是AsnycTask的實現基礎
在使用Thread創建單個線程執行耗時任務的場景下,線程個數有限的前提下是可以的選擇,當線程個數過多,比如高併發的環境下,創建大量的Thread將會:
1. 每個線程要分配棧內存,消耗大量資源;
2. 頻繁進行線程間切換,顯著降低性能;
此時Thread的缺點就凸顯了出來。
2.2 AsyncTask的原理
異步任務的基本認識:
- 構造時指定Params, Progress, Result三個泛型
- 在耗時操作執行之前,可以在onPreExecute()方法中預處理;
- 在doInBackgroud()方法中執行耗時操作;
- 在doInBackgroud()方法中可以調用publishProgress()可以在onProressUpdate()中更新進度;
- 任務執行完成時,在onPostExecute()方法中獲取結果;
主要看下doInBackgroud()方法是怎樣在線程中執行並最終得到結果的:
- 在AsyncTask類加載時,創建了三個Exector對象:一個backupExecutors(用於處理不可執行的任務)、一個ThreadPoolExecutor線程池對象、一個管理Runnable雙頭隊列的執行器;其中線程池是一個核心線程爲1,非核心線程爲20的線程池;
// AsyncTask.java
// 1. 不可執行任務的處理器
private static ThreadPoolExecutor sBackupExecutor;
// 2. 真正執行耗時操作的線程池
public static final Executor THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE/*1*/, MAXIMUM_POOL_SIZE/*20*/, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), sThreadFactory);
//...
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
// 3. 帶雙頭隊列的執行器 管理任務隊列
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
- 在AsyncTask實例化時,初始化了MainHandler、Callable、FutureTask;
// AsyncTask.java
public AsyncTask(@Nullable Looper callbackLooper) {
mHandler = ...;
mWorker = new WorkerRunnable<Params, Result>() {
// 實現了call方法 在後面會被調用
public Result call() throws Exception {
//...
// 1. 設置優先級
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// 2. 觸發耗時業務方法
result = doInBackground(mParams);
// 3. 傳遞結果
postResult(result);
return result;
}
};
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {// 確保結果被回傳
postResultIfNotInvoked(get());
//...
}
};
}
- 在調用execute(Params…)方法時,觸發executeOnExecutor方法,使用Runnable隊列執行器執行並傳入參數;
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
- 其中executeOnExecutor會檢查狀態、調用onPreExecute對數據預處理(如果重寫了的話,否則空實現)、使用隊列執行器執行隊列中的任務,要執行的任務是FutureTask;(這裏有個疑惑:我們也沒有顯示創建這個FutureTask對象,他是構造方法創建的,怎麼知道我要執行什麼呢?)
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
//... 檢查和更新狀態
// 1. 數據預處理
onPreExecute();
// 2. 對Callable(的子類)賦值參數
mWorker.mParams = params;
// 3. 使用任務隊列管理執行器執行 傳入了AsyncTask構造方法中實例化的FutureTask對象
exec.execute(mFuture);
return this;
}
private static class SerialExecutor implements Executor {
// 管理的雙頭隊列
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
// 4. 上步的exec默認就是這個執行器,接下來執行此方法
public synchronized void execute(final Runnable r) {
// 5. 向隊列中遞交任務,被遞交的匿名內部類Runnable不會立即執行而是等其他執行器從此隊列取出然後執行內部類的run,進而執行FutureTask的run
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();// 此處就是執行FutureTask.run的
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
// 6. 取出任務使用線程池執行
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
// 此時執行的就是FutureTask的run 進而執行Callable的call 進而執行doInBackground
// mActive就是從mTasks中取出的Runnable
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
- 隊列執行器向隊列中遞交(offer)任務,在線程池中取出(poll)任務,此時執行FutureTask的run方法,;這時候就進入了2.1中補充內容的流程:
// FutureTask.java
// run() --> callable.call() --> set(result) --> finishCompletion() --> done() --> 重寫done() 中調用get() --> report()
public void run() {
// ...
try {
// 1. 調用Callable.call()
result = c.call();
ran = true;
} catch (Throwable ex) {
// ...
}
if (ran)
// 2. 執行set
set(result);
// ...
}
protected void set(V v) {
//...
outcome = v;// 1. 對Result outcome賦值
finishCompletion();// 2. 觸發finishCompletion
}
private void finishCompletion() {
//...
done(); //調用done 最終執行FutueTask的重寫的done
}
- 構造方法中初始化的Callable就被觸發了,進入其重寫的call方法,設置了線程優先級和調用doInBackgroud();至此工作線程開始執行自定的耗時業務!並在得到結果時觸發postResult–>postExecute;
2.3 HandlerThread和IntentService
2.3.1 HandlerThread
對於主線程,由於創建時由Zygote孵化出來,在Framework中就執行了prepareMainLooper()、loop(),因此主線程在啓動時就已經有Looper了,而工作線程如果想使用Handler,需要手動的prepare、loop、quit。於是出現了HandlerThread,由官方封裝的無需手動操作的帶Looper的工作線程。
public class HandlerThread extends Thread {
// 1. 構造方法中執行了父類Thread的構造方法和優先級的初始化
// 2. 在調用start後出發run執行了下面的操作
@Override
public void run() {
Looper.prepare();// 2.1 輪詢器準備
synchronized (this) {//2.2 同步獲取輪詢器引用 並釋放對象鎖
mLooper = Looper.myLooper();
notifyAll();
}
onLooperPrepared();// 2.3 準備完有一個方法可以按需重寫做業務操作
Looper.loop();// 2.4 開始輪詢
}
}
HandlerThread完成了Looper的準備、輪詢,這個時候只需要創建一個綁定這個Looper的Handler,然後向Handler發送消息,即可在handleMessage方法中執行耗時操作。這一點不同於Thread在Runnable的run方法,線程池在Executor.execute方法中執行耗時操作,在工作線程Handler執行完的結果也可以確切的在用消息機制傳遞到主線程Looper的Handler更新UI;
2.3.2 IntentService
通過下面的源碼步驟可以明顯看出IntentService就是在Service中使用HandlerThread
其優點是在需要使用Service來在後臺執行耗時任務時,無需創建和管理工作線程,而可以直接在onHandleIntent方法中執行耗時操作,並且執行完成後,會自動幫我們stopSelf。
在多次start相同的IntentService時,onStartCommand會依次向ServiceHandler中發消息,並在完成任務後自動停止,注意不要重寫onBind 不要使用bind方式啓動IntentService。
// IntentService.java
@Override
public void onCreate() {
super.onCreate();
// 1. 創建並啓動了HandlerThread
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();
// 2. 獲取HandlerThread的Looper並綁定Handler
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
private final class ServiceHandler extends Handler {
//...
@Override
public void handleMessage(Message msg) {
// 3. 調用此方法執行消息處理
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
2.4 Executors線程池原理
常用的線程池比如:
- SingleThreadPool(維護一個單線程的線程池);
- FixedThreadPool(維護一個指定核心線程數個數的線程池,並且所有線程都是核心線程);
- CacheThreadPool(維護一個最大爲Integer.MAX_VALUE的非核心線程池,所有線程都是非核心線程);
- ScheduleThreadPool(可以調度在合適時間執行任務的線程池)
看一下他們的實現方式就發現1~3都是由ThreadPoolExecutor來實現,4特殊一點是ScheduleThreadPoolExecutor
下面看下UML圖
ThreadPoolExecutor的重要成員變量和execute邏輯:
- 表示線程池狀態和線程池中線程個數的原子Int值——ctl
// 初始值是0b1110 0000 0000 0000 0000 0000 0000 0000 表示線程池RUNNING狀態 池中0個線程
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
- 再看下ctlOf方法和幾個狀態:
其中COUNT_BITS=29(十進制),各個狀態值都是左移29位表示捨棄低29位,意思就是高3位的是狀態值數據:各個狀態高三位的數據是:
ctlOf就是一個邏輯與操作;
Run Status | 0b Value (hight 3 bit) |
---|---|
RUNNING | 111 (二進制,負數) |
SHUTDOWN | 000 |
STOP | 001 |
TIDYING | 010 |
TERMI | 011 |
// 0b1110 0000 0000 0000 0000 0000 0000 0000
private static final int RUNNING = -1 << COUNT_BITS;
// 0b0000 0000 0000 0000 0000 0000 0000 0000 在線程池調用shutdown時設爲此狀態 與STOP的區別是停止接受任務,但已經入隊的任務會被執行完,再完全終止
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 0b0010 0000 0000 0000 0000 0000 0000 0000 在線程池調用shutdownNow時設爲此狀態 停止接受任務,並嘗試中斷正在執行的任務,返回被中斷的任務列表,再完全終止
private static final int STOP = 1 << COUNT_BITS;
// 0b0100 0000 0000 0000 0000 0000 0000 0000 完全終止前的一箇中間狀態
private static final int TIDYING = 2 << COUNT_BITS;
// 0b1100 0000 0000 0000 0000 0000 0000 0000 線程池完全終止
private static final int TERMINATED = 3 << COUNT_BITS;
- execute的邏輯:
主要看下邏輯主線,細節省略研究。
public void execute(Runnable command) {
...
//1. 根據ctl低29位判斷線程個數是否少於核心線程數,是則調用addWorker(command, true)
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 接addWorker方法第5步,返回false的情況:
// a. 線程池被關閉釋放(除了SHUTDOWN時任務未執行完時提交空任務);
// b. 線程個數超過核心線程個數。
//
// 3. 當線程池是RUNNING狀態,向阻塞式隊列添加任務,隊列未滿添加成功返回true,進入代碼塊;否則跳過
// Fixed/Single線程池傳入的是ListedBlockingQueue(由鏈表實現的阻塞式隊列,FIFO,隊列的容量是Integer.MAX_VALUE)
// offer是非阻塞方法,直接向隊列中添加任務,如果隊列滿了返回false
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 重新檢查運行狀態,當線程池不是RUNNING,則移除任務,移除成功進入代碼塊(拒絕執行任邏輯)
if (! isRunning(recheck) && remove(command))
...
else if (workerCountOf(recheck) == 0)
// 4. 線程池仍然是RUNNING狀態或者非RUNNING沒有移除成功,線程池線程個數爲0,添加非核心線程,
// 在Cache線程池(全部非核心),線程執行完任務全部超時退出後會執行這一步
addWorker(null, false);
}
// 5. 添加非核心線程執行任務,此時要求保證線程個數不能超過最大線程數,否則進入代碼塊(拒絕執行任邏輯)
else if (!addWorker(command, false))
...
}
小結:
在execute方法中主要是通過檢查線程池中的線程個數來決定是a.新增線程執行、b.放入任務隊列、c.拒絕執行 的方式。
如果 線程個數 < 核心線程數,直接創建啓動新線程執行任務;
如果 線程個數 > 核心線程數,根據是RUNNING狀態嘗試向任務隊列添加任務;
如果非RUNNING,或者RUNNING但任務入隊不成功(任務隊列滿了),那麼判斷線程個數 < 最大線程個數,嘗試啓動非核心線程執行任務,超過最大線程個數或者線程池在退出釋放就會拒絕執行。
下面是更具體的源碼分析:
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
...
for (;;) {
// 2. 根據ctl低29位判斷線程個數是否大於核心線程數(此時core=true,未超過壓力值(2^29-1)
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 3. 線程個數未發生變化,增加一個線程個數退出循環;否則重新獲取ctl並繼續循環
// * 在線程退出釋放或啓動失敗時會減小這個值
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get();
...
}
}
...
// 4. 創建Worker實例(封裝了用工廠創建線程),將其添加到集合用於管理,添加成功標記爲true時啓動線程
w = new Worker(firstTask);
final Thread t = w.thread;
...
workers.add(w);
...
workerAdded = true;
...
if (workerAdded) {
// 5. 啓動線程...
t.start();
workerStarted = true;
}
...
if (! workerStarted) // 添加失敗回調
addWorkerFailed(w);
return workerStarted;
}
在Worker實例中的線程啓動後,調用run() --> runWorker() -->提交的任務的run()
final void runWorker(Worker w) {
...
while (task != null || (task = getTask()) != null) {
...
// 5. task就是execute中提交的任務,在線程池中有兩個來源一個是Worker實例創建是初始化的,一個是阻塞隊裏中的,只有任務都爲空纔會退出循環、退出線程;
task.run();
...
}
...
processWorkerExit(w, completedAbruptly);
}
private Runnable getTask() {
...
for (;;) {
...
// allow這個是false 先不管
// 如果線程個數大於核心線程數,阻塞指定的保活時間後返回,有可能阻塞或得到了隊列中的任務也有可能是null;
// 如果小於或等於核心線程數,一直阻塞直到隊列中有任務!
// 這裏保證了不關閉線程池,核心線程一直存活,而非核心線程超時退出釋放!!!
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
...
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
...
}
}
總結:
在線程池中核心線程、非核心線程實質上並沒有用於確定身份的標記;
只要小於核心線程數,創建出來就是核心線程,大於就是非核心,區別在於核心執行完任務會在阻塞隊列中等任務不會被退出釋放,而非核心執行完任務並且隊列中在超時前沒有任務就會退出;
換言之,線程池總是維護指定個數的核心線程長期執行任務,非核心只在任務爆滿時臨時創建並執行,之後非核心會超時退出釋放。
3.併發變成Netty中線程池、事件循環(附)
專題系列再總結