Android 性能優化之使用線程池處理異步任務

轉自:http://android.jobbole.com/82092/android

 

說到線程,我想你們都不陌生,由於在開發時候或多或少都會用到線程,而一般建立線程有兩種方式:api

一、繼承Thread類
二、實現Runnable接口緩存

雖然說這兩種方式均可以建立出一個線程,不過它們之間仍是有一點區別的,主要區別在於在多線程訪問同一資源的狀況下,用Runnable接口建立的線程能夠處理同一資源,而用Thread類建立的線程則各自獨立處理,各自擁有本身的資源。多線程

因此,在Java中大多數多線程程序都是經過實現Runnable來完成的,而對於Android來講也不例外,當涉及到須要開啓線程去完成某件事時,咱們都會這樣寫:併發

這段代碼建立了一個線程並執行,它在任務結束後GC會自動回收該線程,一切看起來如此美妙,是的,它在線程併發很少的程序中確實不錯,而假如這個程序有不少地方須要開啓大量線程來處理任務,那麼若是仍是用上述的方式去建立線程處理的話,那麼將致使系統的性能表現的很是糟糕,更別說在內存有限的移動設備上,主要的影響以下:eclipse

一、線程的建立和銷燬都須要時間,當有大量的線程建立和銷燬時,那麼這些時間的消耗則比較明顯,將致使性能上的缺失異步

二、大量的線程建立、執行和銷燬是很是耗cpu和內存的,這樣將直接影響系統的吞吐量,致使性能急劇降低,若是內存資源佔用的比較多,還極可能形成OOMasync

三、大量的線程的建立和銷燬很容易致使GC頻繁的執行,從而發生內存抖動現象,而發生了內存抖動,對於移動端來講,最大的影響就是形成界面卡頓ide

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

ExecutorService

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

使用線程池管理線程的優勢

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

二、線程池旨在線程的複用,這就能夠節約咱們用以往的方式建立線程和銷燬所消耗的時間,減小線程頻繁調度的開銷,從而節約系統資源,提升系統吞吐量

三、在執行大量異步任務時提升了性能

四、Java內置的一套ExecutorService線程池相關的api,能夠更方便的控制線程的最大併發數、線程的定時任務、單線程的順序執行等

ExecutorService簡介

一般來講咱們說到線程池第一時間想到的就是它:ExecutorService,它是一個接口,其實若是要從真正意義上來講,它能夠叫作線程池的服務,由於它提供了衆多接口api來控制線程池中的線程,而真正意義上的線程池就是:ThreadPoolExecutor,它實現了ExecutorService接口,並封裝了一系列的api使得它具備線程池的特性,其中包括工做隊列、核心線程數、最大線程數等。

線程池:ThreadPoolExecutor

既然線程池就是ThreadPoolExecutor,因此咱們要建立一個線程池只須要new ThreadPoolExecutor(…);就能夠建立一個線程池,而若是這樣建立線程池的話,咱們須要配置一堆東西,很是麻煩,咱們能夠看一下它的構造方法就知道了:

因此,官方也不推薦使用這種方法來建立線程池,而是推薦使用Executors的工廠方法來建立線程池,Executors類是官方提供的一個工廠類,它裏面封裝好了衆多功能不同的線程池,從而使得咱們建立線程池很是的簡便,主要提供了以下五種功能不同的線程池:

一、newFixedThreadPool() :
做用:該方法返回一個固定線程數量的線程池,該線程池中的線程數量始終不變,即不會再建立新的線程,也不會銷燬已經建立好的線程,自始自終都是那幾個固定的線程在工做,因此該線程池能夠控制線程的最大併發數。
栗子:假若有一個新任務提交時,線程池中若是有空閒的線程則當即使用空閒線程來處理任務,若是沒有,則會把這個新任務存在一個任務隊列中,一旦有線程空閒了,則按FIFO方式處理任務隊列中的任務。

二、newCachedThreadPool() :
做用:該方法返回一個能夠根據實際狀況調整線程池中線程的數量的線程池。即該線程池中的線程數量不肯定,是根據實際狀況動態調整的。
栗子:假如該線程池中的全部線程都正在工做,而此時有新任務提交,那麼將會建立新的線程去處理該任務,而此時假如以前有一些線程完成了任務,如今又有新任務提交,那麼將不會建立新線程去處理,而是複用空閒的線程去處理新任務。那麼此時有人有疑問了,那這樣來講該線程池的線程豈不是會越集越多?其實並不會,由於線程池中的線程都有一個「保持活動時間」的參數,經過配置它,若是線程池中的空閒線程的空閒時間超過該「保存活動時間」則馬上中止該線程,而該線程池默認的「保持活動時間」爲60s。

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

四、newScheduledThreadPool() :
做用:該方法返回一個能夠控制線程池內線程定時或週期性執行某任務的線程池。

五、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:一般叫作拒絕策略,一、在線程池已經關閉的狀況下 二、任務太多致使最大線程數和任務隊列已經飽和,沒法再接收新的任務 。在上面兩種狀況下,只要知足其中一種時,在使用execute()來提交新的任務時將會拒絕,而默認的拒絕策略是拋一個RejectedExecutionException異常

上面的參數理解起來都比較簡單,不過workQueue這個任務隊列卻要再次說明一下,它是一個BlockingQueue對象,而泛型則限定它是用來存放Runnable對象的,剛剛上面講了,不一樣的線程池它的任務隊列實現確定是不同的,因此,保證不一樣線程池有着不一樣的功能的核心就是這個workQueue的實現了,細心的會發如今剛剛的用來建立線程池的工廠方法中,針對不一樣的線程池傳入的workQueue也不同,下面我總結一下這五種線程池分別用的是什麼BlockingQueue:

一、newFixedThreadPool()—>LinkedBlockingQueue
二、newSingleThreadExecutor()—>LinkedBlockingQueue
三、newCachedThreadPool()—>SynchronousQueue
四、newScheduledThreadPool()—>DelayedWorkQueue
五、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來實現一個功能是按任務的優先級來處理的線程池。

一、首先咱們建立一個基於PriorityBlockingQueue實現的線程池,爲了測試方便,我這裏把核心線程數量設置爲3,以下:

 

 

二、而後建立一個實現Runnable接口的類,並向外提供一個抽象方法供咱們實現自定義功能,並實現Comparable接口,實現這個接口主要就是進行優先級的比較,代碼以下:

 

 

三、使用咱們本身的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,這兩個方法各有不一樣,能夠根據實際需求方便的運用,以下:

一、shutdown()方法在終止前容許執行之前提交的任務。
二、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這個參數就是一個固定線程數量的線程池:

相關文章
相關標籤/搜索