Android 效能優化之使用執行緒池處理非同步任務

Sunzxyong發表於2015-11-19

說到執行緒,我想大家都不陌生,因為在開發時候或多或少都會用到執行緒,而通常建立執行緒有兩種方式:

1、繼承Thread類
2、實現Runnable介面

雖說這兩種方式都可以建立出一個執行緒,不過它們之間還是有一點區別的,主要區別在於在多執行緒訪問同一資源的情況下,用Runnable介面建立的執行緒可以處理同一資源,而用Thread類建立的執行緒則各自獨立處理,各自擁有自己的資源。

所以,在Java中大多數多執行緒程式都是通過實現Runnable來完成的,而對於Android來說也不例外,當涉及到需要開啟執行緒去完成某件事時,我們都會這樣寫:

這段程式碼建立了一個執行緒並執行,它在任務結束後GC會自動回收該執行緒,一切看起來如此美妙,是的,它線上程併發不多的程式中確實不錯,而假如這個程式有很多地方需要開啟大量執行緒來處理任務,那麼如果還是用上述的方式去建立執行緒處理的話,那麼將導致系統的效能表現的非常糟糕,更別說在記憶體有限的移動裝置上,主要的影響如下:

1、執行緒的建立和銷燬都需要時間,當有大量的執行緒建立和銷燬時,那麼這些時間的消耗則比較明顯,將導致效能上的缺失

2、大量的執行緒建立、執行和銷燬是非常耗cpu和記憶體的,這樣將直接影響系統的吞吐量,導致效能急劇下降,如果記憶體資源佔用的比較多,還很可能造成OOM

3、大量的執行緒的建立和銷燬很容易導致GC頻繁的執行,從而發生記憶體抖動現象,而發生了記憶體抖動,對於移動端來說,最大的影響就是造成介面卡頓

而針對上述所描述的問題,解決的辦法歸根到底就是:重用已有的執行緒,從而減少執行緒的建立。
所以這就涉及到執行緒池(ExecutorService)的概念了,執行緒池的基本作用就是進行執行緒的複用,下面將具體介紹執行緒池的使用

ExecutorService

通過上述分析,我們知道了通過new Thread().start()方式建立執行緒去處理任務的弊端,而為了解決這些問題,Java為我們提供了ExecutorService執行緒池來優化和管理執行緒的使用

使用執行緒池管理執行緒的優點

1、執行緒的建立和銷燬由執行緒池維護,一個執行緒在完成任務後並不會立即銷燬,而是由後續的任務複用這個執行緒,從而減少執行緒的建立和銷燬,節約系統的開銷

2、執行緒池旨線上程的複用,這就可以節約我們用以往的方式建立執行緒和銷燬所消耗的時間,減少執行緒頻繁排程的開銷,從而節約系統資源,提高系統吞吐量

3、在執行大量非同步任務時提高了效能

4、Java內建的一套ExecutorService執行緒池相關的api,可以更方便的控制執行緒的最大併發數、執行緒的定時任務、單執行緒的順序執行等

ExecutorService簡介

通常來說我們說到執行緒池第一時間想到的就是它:ExecutorService,它是一個介面,其實如果要從真正意義上來說,它可以叫做執行緒池的服務,因為它提供了眾多介面api來控制執行緒池中的執行緒,而真正意義上的執行緒池就是:ThreadPoolExecutor,它實現了ExecutorService介面,並封裝了一系列的api使得它具有執行緒池的特性,其中包括工作佇列、核心執行緒數、最大執行緒數等。

執行緒池:ThreadPoolExecutor

既然執行緒池就是ThreadPoolExecutor,所以我們要建立一個執行緒池只需要new ThreadPoolExecutor(…);就可以建立一個執行緒池,而如果這樣建立執行緒池的話,我們需要配置一堆東西,非常麻煩,我們可以看一下它的構造方法就知道了:

所以,官方也不推薦使用這種方法來建立執行緒池,而是推薦使用Executors的工廠方法來建立執行緒池,Executors類是官方提供的一個工廠類,它裡面封裝好了眾多功能不一樣的執行緒池,從而使得我們建立執行緒池非常的簡便,主要提供瞭如下五種功能不一樣的執行緒池:

1、newFixedThreadPool() :
作用:該方法返回一個固定執行緒數量的執行緒池,該執行緒池中的執行緒數量始終不變,即不會再建立新的執行緒,也不會銷燬已經建立好的執行緒,自始自終都是那幾個固定的執行緒在工作,所以該執行緒池可以控制執行緒的最大併發數。
栗子:假如有一個新任務提交時,執行緒池中如果有空閒的執行緒則立即使用空閒執行緒來處理任務,如果沒有,則會把這個新任務存在一個任務佇列中,一旦有執行緒空閒了,則按FIFO方式處理任務佇列中的任務。

2、newCachedThreadPool() :
作用:該方法返回一個可以根據實際情況調整執行緒池中執行緒的數量的執行緒池。即該執行緒池中的執行緒數量不確定,是根據實際情況動態調整的。
栗子:假如該執行緒池中的所有執行緒都正在工作,而此時有新任務提交,那麼將會建立新的執行緒去處理該任務,而此時假如之前有一些執行緒完成了任務,現在又有新任務提交,那麼將不會建立新執行緒去處理,而是複用空閒的執行緒去處理新任務。那麼此時有人有疑問了,那這樣來說該執行緒池的執行緒豈不是會越集越多?其實並不會,因為執行緒池中的執行緒都有一個“保持活動時間”的引數,通過配置它,如果執行緒池中的空閒執行緒的空閒時間超過該“儲存活動時間”則立刻停止該執行緒,而該執行緒池預設的“保持活動時間”為60s。

3、newSingleThreadExecutor() :
作用:該方法返回一個只有一個執行緒的執行緒池,即每次只能執行一個執行緒任務,多餘的任務會儲存到一個任務佇列中,等待這一個執行緒空閒,當這個執行緒空閒了再按FIFO方式順序執行任務佇列中的任務。

4、newScheduledThreadPool() :
作用:該方法返回一個可以控制執行緒池內執行緒定時或週期性執行某任務的執行緒池。

5、newSingleThreadScheduledExecutor() :
作用:該方法返回一個可以控制執行緒池內執行緒定時或週期性執行某任務的執行緒池。只不過和上面的區別是該執行緒池大小為1,而上面的可以指定執行緒池的大小。

好了,寫了一堆來介紹這五種執行緒池的作用,接下來就是獲取這五種執行緒池,通過Executors的工廠方法來獲取:

我們可以看到通過Executors的工廠方法來建立執行緒池極其簡便,其實它的內部還是通過new ThreadPoolExecutor(…)的方式建立執行緒池的,我們看一下這些工廠方法的內部實現:

我們可以清楚的看到這些方法的內部實現都是通過建立一個ThreadPoolExecutor物件來建立的,正所謂萬變不離其宗,所以我們要了解執行緒池還是得了解ThreadPoolExecutor這個執行緒池類,其中由於和定時任務相關的執行緒池比較特殊(newScheduledThreadPool()、newSingleThreadScheduledExecutor()),它們建立的執行緒池內部實現是由ScheduledThreadPoolExecutor這個類實現的,而ScheduledThreadPoolExecutor是繼承於ThreadPoolExecutor擴充套件而成的,所以本質還是一樣的,只不過多封裝了一些定時任務相關的api,所以我們主要就是要了解ThreadPoolExecutor,從構造方法開始:

我們可以看到它構造方法的引數比較多,有七個,下面一一來說明這些引數的作用:

corePoolSize:執行緒池中的核心執行緒數量
maximumPoolSize:執行緒池中的最大執行緒數量
keepAliveTime:這個就是上面說到的“保持活動時間“,上面只是大概說明了一下它的作用,不過它起作用必須在一個前提下,就是當執行緒池中的執行緒數量超過了corePoolSize時,它表示多餘的空閒執行緒的存活時間,即:多餘的空閒執行緒在超過keepAliveTime時間內沒有任務的話則被銷燬。而這個主要應用在快取執行緒池中
unit:它是一個列舉型別,表示keepAliveTime的單位,常用的如:TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)
workQueue:任務佇列,主要用來儲存已經提交但未被執行的任務,不同的執行緒池採用的排隊策略不一樣,稍後再講
threadFactory:執行緒工廠,用來建立執行緒池中的執行緒,通常用預設的即可
handler:通常叫做拒絕策略,1、線上程池已經關閉的情況下 2、任務太多導致最大執行緒數和任務佇列已經飽和,無法再接收新的任務 。在上面兩種情況下,只要滿足其中一種時,在使用execute()來提交新的任務時將會拒絕,而預設的拒絕策略是拋一個RejectedExecutionException異常

上面的引數理解起來都比較簡單,不過workQueue這個任務佇列卻要再次說明一下,它是一個BlockingQueue物件,而泛型則限定它是用來存放Runnable物件的,剛剛上面講了,不同的執行緒池它的任務佇列實現肯定是不一樣的,所以,保證不同執行緒池有著不同的功能的核心就是這個workQueue的實現了,細心的會發現在剛剛的用來建立執行緒池的工廠方法中,針對不同的執行緒池傳入的workQueue也不一樣,下面我總結一下這五種執行緒池分別用的是什麼BlockingQueue:

1、newFixedThreadPool()—>LinkedBlockingQueue
2、newSingleThreadExecutor()—>LinkedBlockingQueue
3、newCachedThreadPool()—>SynchronousQueue
4、newScheduledThreadPool()—>DelayedWorkQueue
5、newSingleThreadScheduledExecutor()—>DelayedWorkQueue

這些佇列分別表示:

LinkedBlockingQueue:無界的佇列
SynchronousQueue:直接提交的佇列
DelayedWorkQueue:等待佇列

當然實現了BlockingQueue介面的佇列還有:ArrayBlockingQueue(有界的佇列)、PriorityBlockingQueue(優先順序佇列)。這些佇列的詳細作用就不多介紹了。

執行緒池ThreadPoolExecutor的使用

使用執行緒池,其中涉及到一個極其重要的方法,即:

該方法意為執行給定的任務,該任務處理可能在新的執行緒、已入池的執行緒或者正呼叫的執行緒,這由ThreadPoolExecutor的實現決定。
newFixedThreadPool
建立一個固定執行緒數量的執行緒池,示例為:

上述程式碼,我們建立了一個執行緒數為3的固定執行緒數量的執行緒池,同理該執行緒池支援的執行緒最大併發數也是3,而我模擬了10個任務讓它處理,執行的情況則是首先執行前三個任務,後面7個則依次進入任務佇列進行等待,執行完前三個任務後,再通過FIFO的方式從任務佇列中取任務執行,直到最後任務都執行完畢。
為了體現出執行緒的複用,我特地在Log中加上了當前執行緒的名稱,效果為:
這裡寫圖片描述
newSingleThreadExecutor
建立一個只有一個執行緒的執行緒池,每次只能執行一個執行緒任務,多餘的任務會儲存到一個任務佇列中,等待執行緒處理完再依次處理任務佇列中的任務,示例為:

程式碼還是差不多,只不過改了執行緒池的實現方式,效果我想大家都知道,即依次一個一個的處理任務,而且都是複用一個執行緒,效果為:
這裡寫圖片描述

其實我們通過newSingleThreadExecutor()和newFixedThreadPool()的方法發現,建立一個singleThreadExecutorPool實際上就是建立一個核心執行緒數和最大執行緒數都為1的fixedThreadPool。
newCachedThreadPool
建立一個可以根據實際情況調整執行緒池中執行緒的數量的執行緒池,示例為:

為了體現該執行緒池可以自動根據實現情況進行執行緒的重用,而不是一味的建立新的執行緒去處理任務,我設定了每隔1s去提交一個新任務,這個新任務執行的時間也是動態變化的,所以,效果為:
這裡寫圖片描述
newScheduledThreadPool
建立一個可以定時或者週期性執行任務的執行緒池,示例為:

newSingleThreadScheduledExecutor
建立一個可以定時或者週期性執行任務的執行緒池,該執行緒池的執行緒數為1,示例為:

實際上這個和上面的沒什麼太大區別,只不過是執行緒池內執行緒數量的不同,效果為:
這裡寫圖片描述
每隔2秒就會執行一次該任務

自定義執行緒池ThreadPoolExecutor

Java內建只為我們提供了五種常用的執行緒池,一般來說這足夠用了,不過有時候我們也可以根據需求來自定義我們自己的執行緒池,而要自定義不同功能的執行緒池,上面我們也說了執行緒池功能的不同歸根到底還是內部的BlockingQueue實現不同,所以,我們要實現我們自己相要的執行緒池,就必須從BlockingQueue的實現上做手腳,而上面也說了BlockingQueue的實現類有多個,那麼這次我們就選用PriorityBlockingQueue來實現一個功能是按任務的優先順序來處理的執行緒池。

1、首先我們建立一個基於PriorityBlockingQueue實現的執行緒池,為了測試方便,我這裡把核心執行緒數量設定為3,如下:

2、然後建立一個實現Runnable介面的類,並向外提供一個抽象方法供我們實現自定義功能,並實現Comparable介面,實現這個介面主要就是進行優先順序的比較,程式碼如下:

3、使用我們自己的PriorityRunnable提交任務,整體程式碼如下:

測試效果

我們看下剛剛自定義的執行緒池是否達到了我們想要的功能,即根據任務的優先順序進行優先處理任務,效果如下:
這裡寫圖片描述

可以從執行結果中看出,由於核心執行緒數設定為3,剛開始時,系統有3個空閒執行緒,所以無須使用任務佇列,而是直接執行前三個任務,而後面再提交任務時由於當前沒有空閒執行緒所以加入任務佇列中進行等待,此時,由於我們的任務佇列實現是由PriorityBlockingQueue實現的,所以進行等待的任務會經過優先順序判斷,優先順序高的放在佇列前面先處理。從效果圖中也可以看到後面的任務是先執行優先順序高的任務,然後依次遞減。

優先順序執行緒池的優點

從上面我們可以得知,建立一個優先順序執行緒池非常有用,它可以線上程池中執行緒數量不足或系統資源緊張時,優先處理我們想要先處理的任務,而優先順序低的則放到後面再處理,這極大改善了系統預設執行緒池以FIFO方式處理任務的不靈活

擴充套件執行緒池ThreadPoolExecutor

除了內建的功能外,ThreadPoolExecutor也向外提供了三個介面供我們自己擴充套件滿足我們需求的執行緒池,這三個介面分別是:

beforeExecute() – 任務執行前執行的方法
afterExecute() -任務執行結束後執行的方法
terminated() -執行緒池關閉後執行的方法

這三個方法在ThreadPoolExecutor內部都沒有實現

前面兩個方法我們可以在ThreadPoolExecutor內部的runWorker()方法中找到,而runWorker()是ThreadPoolExecutor的內部類Worker實現的方法,Worker它實現了Runnable介面,也正是執行緒池內處理任務的工作執行緒,而Worker.runWorker()方法則是處理我們所提交的任務的方法,它會同時被多個執行緒訪問,所以我們看runWorker()方法的實現,由於涉及到多個執行緒的非同步呼叫,必然是需要使用鎖來處理,而這裡使用的是Lock來實現的,我們來看看runWorker()方法內主要實現:
這裡寫圖片描述

可以看到在task.run()之前和之後分別呼叫了beforeExecute和afterExecute方法,並傳入了我們的任務Runnable物件

而terminated()則是在關閉執行緒池的方法中呼叫,而關閉執行緒池有兩個方法,我貼其中一個:
這裡寫圖片描述

所以,我們要擴充套件執行緒池,只需要重寫這三個方法,並實現我們自己的功能即可,這三個方法分別都會在任務執行前呼叫、任務執行完成後呼叫、執行緒池關閉後呼叫。
這裡我驗證一下,繼承自ThreadPoolExecutor 並實現那三個方法:

而執行後的結果則是,這正符合剛剛說的:

所以,在上面我們的優先順序執行緒池的程式碼上,我們再擴充套件一個具有暫停功能的優先順序執行緒池,程式碼如下:
具有暫時功能的執行緒池:

然後結合上面的優先順序執行緒池的實現,建立具有暫停功能的優先順序執行緒池:

這裡我為了演示效果,把這個執行緒池設為只有一個執行緒,然後直接在TextView中顯示當前執行的任務的優先順序,然後設定個開關,控制執行緒池的暫停與開始:

效果為:
這裡寫圖片描述

從效果上來看,該執行緒池和優先順序執行緒一樣,而且還多了一個暫停與開始的功能

優化執行緒池ThreadPoolExecutor

雖說執行緒池極大改善了系統的效能,不過建立執行緒池也是需要資源的,所以執行緒池內執行緒數量的大小也會影響系統的效能,大了反而浪費資源,小了反而影響系統的吞吐量,所以我們建立執行緒池需要把握一個度才能合理的發揮它的優點,通常來說我們要考慮的因素有CPU的數量、記憶體的大小、併發請求的數量等因素,按需調整。

通常核心執行緒數可以設為CPU數量+1,而最大執行緒數可以設為CPU的數量*2+1。

獲取CPU數量的方法為:

shutdown()和shutdownNow()的區別

關於執行緒池的停止,ExecutorService為我們提供了兩個方法:shutdown和shutdownNow,這兩個方法各有不同,可以根據實際需求方便的運用,如下:

1、shutdown()方法在終止前允許執行以前提交的任務。
2、shutdownNow()方法則是阻止正在任務佇列中等待任務的啟動並試圖停止當前正在執行的任務。

關於AsyncTask的實現

大家都知道AsyncTask內部實現其實就是Thread+Handler。其中Handler是為了處理執行緒之間的通訊,而這個Thread到底是指什麼呢?通過AsyncTask原始碼可以得知,其實這個Thread是執行緒池,AsyncTask內部實現了兩個執行緒池,分別是:序列執行緒池和固定執行緒數量的執行緒池。而這個固定執行緒數量則是通過CPU的數量決定的。

在預設情況下,我們大都通過AsyncTask::execute()來執行任務的,
,而execute()內部則是呼叫executeOnExecutor(sDefaultExecutor, params)方法執行的,第一個引數就是指定處理該任務的執行緒池,而預設情況下AsyncTask是傳入序列執行緒池(在這裡不講版本的變化),也就是任務只能單個的按順序執行,而我們要是想讓AsyncTask並行的處理任務,大家都知道呼叫AsyncTask::executeOnExecutor(sDefaultExecutor, params)方法傳入這個引數即可:AsyncTask.THREAD_POOL_EXECUTOR。
而這個引數的意義在於為任務指定了一個固定執行緒數量的執行緒池去處理,從而達到了並行處理的功能,我們可以在原始碼中看到AsyncTask.THREAD_POOL_EXECUTOR這個引數就是一個固定執行緒數量的執行緒池:

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

Android 效能優化之使用執行緒池處理非同步任務 Android 效能優化之使用執行緒池處理非同步任務

相關文章