知識梳理系列之一——多線程

知識梳理系列之一——多線程

本文簡明的梳理下多線程基本知識

幾個問題

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的類型:
    FutureTask UML圖
    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的原理

異步任務的基本認識:

  1. 構造時指定Params, Progress, Result三個泛型
  2. 在耗時操作執行之前,可以在onPreExecute()方法中預處理;
  3. 在doInBackgroud()方法中執行耗時操作;
  4. 在doInBackgroud()方法中可以調用publishProgress()可以在onProressUpdate()中更新進度;
  5. 任務執行完成時,在onPostExecute()方法中獲取結果;

主要看下doInBackgroud()方法是怎樣在線程中執行並最終得到結果的:

  1. 在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();
  1. 在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());
	        //...
	    }
	};
}
  1. 在調用execute(Params…)方法時,觸發executeOnExecutor方法,使用Runnable隊列執行器執行並傳入參數;
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
	return executeOnExecutor(sDefaultExecutor, params);
}
  1. 其中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);
		}
	}
}
  1. 隊列執行器向隊列中遞交(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
}
  1. 構造方法中初始化的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線程池原理

常用的線程池比如:

  1. SingleThreadPool(維護一個單線程的線程池);
  2. FixedThreadPool(維護一個指定核心線程數個數的線程池,並且所有線程都是核心線程);
  3. CacheThreadPool(維護一個最大爲Integer.MAX_VALUE的非核心線程池,所有線程都是非核心線程);
  4. ScheduleThreadPool(可以調度在合適時間執行任務的線程池)

看一下他們的實現方式就發現1~3都是由ThreadPoolExecutor來實現,4特殊一點是ScheduleThreadPoolExecutor

下面看下UML圖
在這裏插入圖片描述

ThreadPoolExecutor的重要成員變量和execute邏輯:

  1. 表示線程池狀態和線程池中線程個數的原子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;
  1. 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中線程池、事件循環(附)

專題系列再總結

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章