在程序開發(fā)的實(shí)踐當(dāng)中,為了讓程序表現(xiàn)得更加流暢,我們肯定會需要使用到多線程來提升程序的并發(fā)執(zhí)行性能。但是編寫多線程并發(fā)的代碼一直以來都是一個(gè)相對棘手的問題,所以想要獲得更佳的程序性能,我們非常有必要掌握多線程并發(fā)編程的基礎(chǔ)技能。
創(chuàng)新互聯(lián)公司是專業(yè)的鼎城網(wǎng)站建設(shè)公司,鼎城接單;提供做網(wǎng)站、網(wǎng)站制作,網(wǎng)頁設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行鼎城網(wǎng)站開發(fā)網(wǎng)頁制作和功能擴(kuò)展;專業(yè)做搜索引擎喜愛的網(wǎng)站,專業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來合作!
眾所周知,Android 程序的大多數(shù)代碼操作都必須執(zhí)行在主線程,例如系統(tǒng)事件(例如設(shè)備屏幕發(fā)生旋轉(zhuǎn)),輸入事件(例如用戶點(diǎn)擊滑動(dòng)等),程序回調(diào)服務(wù),UI 繪制以及鬧鐘事件等等。那么我們在上述事件或者方法中插入的代碼也將執(zhí)行在主線程。
一旦我們在主線程里面添加了操作復(fù)雜的代碼,這些代碼就很可能阻礙主線程去響應(yīng)點(diǎn)擊/滑動(dòng)事件,阻礙主線程的 UI 繪制等等。我們知道,為了讓屏幕的刷新幀率達(dá)到 60fps,我們需要確保 16ms 內(nèi)完成單次刷新的操作。一旦我們在主線程里面執(zhí)行的任務(wù)過于繁重就可能導(dǎo)致接收到刷新信號的時(shí)候因?yàn)橘Y源被占用而無法完成這次刷新操作,這樣就會產(chǎn)生掉幀的現(xiàn)象,刷新幀率自然也就跟著下降了(一旦刷新幀率降到 20fps 左右,用戶就可以明顯感知到卡頓不流暢了)。
為了避免上面提到的掉幀問題,我們需要使用多線程的技術(shù)方案,把那些操作復(fù)雜的任務(wù)移動(dòng)到其他線程當(dāng)中執(zhí)行,這樣就不容易阻塞主線程的操作,也就減小了出現(xiàn)掉幀的可能性。
那么問題來了,為主線程減輕負(fù)的多線程方案有哪些呢?這些方案分別適合在什么場景下使用?Android 系統(tǒng)為我們提供了若干組工具類來幫助解決這個(gè)問題。
AsyncTask: 為 UI 線程與工作線程之間進(jìn)行快速的切換提供一種簡單便捷的機(jī)制。適用于當(dāng)下立即需要啟動(dòng),但是異步執(zhí)行的生命周期短暫的使用場景。
HandlerThread: 為某些回調(diào)方法或者等待某些任務(wù)的執(zhí)行設(shè)置一個(gè)專屬的線程,并提供線程任務(wù)的調(diào)度機(jī)制。
ThreadPool: 把任務(wù)分解成不同的單元,分發(fā)到各個(gè)不同的線程上,進(jìn)行同時(shí)并發(fā)處理。
IntentService: 適合于執(zhí)行由 UI 觸發(fā)的后臺 Service 任務(wù),并可以把后臺任務(wù)執(zhí)行的情況通過一定的機(jī)制反饋給 UI。
了解這些系統(tǒng)提供的多線程工具類分別適合在什么場景下,可以幫助我們選擇合適的解決方案,避免出現(xiàn)不可預(yù)期的麻煩。雖然使用多線程可以提高程序的并發(fā)量,但是我們需要特別注意因?yàn)橐攵嗑€程而可能伴隨而來的內(nèi)存問題。舉個(gè)例子,在 Activity 內(nèi)部定義的一個(gè) AsyncTask,它屬于一個(gè)內(nèi)部類,該類本身和外面的 Activity 是有引用關(guān)系的,如果 Activity 要銷毀的時(shí)候,AsyncTask 還仍然在運(yùn)行,這會導(dǎo)致 Activity 沒有辦法完全釋放,從而引發(fā)內(nèi)存泄漏。所以說,多線程是提升程序性能的有效手段之一,但是使用多線程卻需要十分謹(jǐn)慎小心,如果不了解背后的執(zhí)行機(jī)制以及使用的注意事項(xiàng),很可能引起嚴(yán)重的問題。
傳統(tǒng)的多線程是通過繼承Thread類及實(shí)現(xiàn)Runnable接口來實(shí)現(xiàn)的,每次創(chuàng)建及銷毀線程都會消耗資源、響應(yīng)速度慢,且線程缺乏統(tǒng)一管理,容易出現(xiàn)阻塞的情況,針對以上缺點(diǎn),線程池就出現(xiàn)了。
線程池是一個(gè)創(chuàng)建使用線程并能保存使用過的線程以達(dá)到復(fù)用的對象,簡單的說就是一塊緩存了一定數(shù)量線程的區(qū)域。
1.復(fù)用線程:線程執(zhí)行完不會立刻退出,繼續(xù)執(zhí)行其他線程;
2.管理線程:統(tǒng)一分配、管理、控制最大并發(fā)數(shù);
1.降低因頻繁創(chuàng)建銷毀線程帶來的性能開銷,復(fù)用緩存在線程池中的線程;
2.提高線程執(zhí)行效率響應(yīng)速度,復(fù)用線程:響應(yīng)速度;管理線程:優(yōu)化線程執(zhí)行順序,避免大量線程搶占資源導(dǎo)致阻塞現(xiàn)象;
3.提高對線程的管理度;
線程池的使用也比較簡單,流程如下:
接下來通過源碼來介紹一下ThreadPoolExecutor內(nèi)部實(shí)現(xiàn)及工作原理。
線程池的最終實(shí)現(xiàn)類是ThreadPoolExecutor,通過實(shí)現(xiàn)可以一步一步的看到,父接口為Executor:
其他的繼承及實(shí)現(xiàn)關(guān)系就不一一列舉了,直接通過以下圖來看一下:
從構(gòu)造方法開始看:
通過以上可以看到,在創(chuàng)建ThreadPoolExecutor時(shí),對傳入的參數(shù)是有要求的:corePoolSize不能小于0;maximumPoolSize需要大于0,且需要大于等于corePoolSize;keepAliveTime大于0;workQueue、threadFactory都不能為null。
在創(chuàng)建完后就需要執(zhí)行Runnable了,看以下execute()方法:
在execute()內(nèi)部主要執(zhí)行的邏輯如下:
分析點(diǎn)1:如果當(dāng)前線程數(shù)未超過核心線程數(shù),則將runnable作為參數(shù)執(zhí)行addWorker(),true表示核心線程,false表示非核心線程;
分析點(diǎn)2:核心線程滿了,如果線程池處于運(yùn)行狀態(tài)則往workQueue隊(duì)列中添加任務(wù),接下來判斷是否需要拒絕或者執(zhí)行addWorker();
分析點(diǎn)3:以上都不滿足時(shí) [corePoolSize=0且沒有運(yùn)行的線程,或workQueue已經(jīng)滿了] ,執(zhí)行addWorker()添加runnable,失敗則執(zhí)行拒絕策略;
總結(jié)一下:線程池對線程創(chuàng)建的管理,流程圖如下:
在執(zhí)行addWorker時(shí),主要做了以下兩件事:
分析點(diǎn)1:將runnable作為參數(shù)創(chuàng)建Worker對象w,然后獲取w內(nèi)部的變量thread;
分析點(diǎn)2:調(diào)用start()來啟動(dòng)thread;
在addWorker()內(nèi)部會將runnable作為參數(shù)傳給Worker,然后從Worker內(nèi)部讀取變量thread,看一下Worker類的實(shí)現(xiàn):
Worker實(shí)現(xiàn)了Runnable接口,在Worker內(nèi)部,進(jìn)行了賦值及創(chuàng)建操作,先將execute()時(shí)傳入的runnable賦值給內(nèi)部變量firstTask,然后通過ThreadFactory.newThread(this)創(chuàng)建Thread,上面講到在addWorker內(nèi)部執(zhí)行t.start()后,會執(zhí)行到Worker內(nèi)部的run()方法,接著會執(zhí)行runWorker(this),一起看一下:
前面可以看到,runWorker是執(zhí)行在子線程內(nèi)部,主要執(zhí)行了三件事:
分析1:獲取當(dāng)前線程,當(dāng)執(zhí)行shutdown()時(shí)需要將線程interrupt(),接下來從Worker內(nèi)部取到firstTask,即execute傳入的runnable,接下來會執(zhí)行;
分析2:while循環(huán),task不空直接執(zhí)行;否則執(zhí)行g(shù)etTask()去獲取,不為空直接執(zhí)行;
分析3:對有效的task執(zhí)行run(),由于是在子線程中執(zhí)行,因此直接run()即可,不需要start();
前面看到,在while內(nèi)部有執(zhí)行g(shù)etTask(),一起看一下:
getTask()是從workQueue內(nèi)部獲取接下來需要執(zhí)行的runnable,內(nèi)部主要做了兩件事:
分析1:先獲取到當(dāng)前正在執(zhí)行工作的線程數(shù)量wc,通過判斷allowCoreThreadTimeOut[在創(chuàng)建ThreadPoolExecutor時(shí)可以進(jìn)行設(shè)置]及wc corePoolSize來確定timed值;
分析2:通過timed值來決定執(zhí)行poll()或者take(),如果WorkQueue中有未執(zhí)行的線程時(shí),兩者作用是相同的,立刻返回線程;如果WorkQueue中沒有線程時(shí),poll()有超時(shí)返回,take()會一直阻塞;如果allowCoreThreadTimeOut為true,則核心線程在超時(shí)時(shí)間沒有使用的話,是需要退出的;wc corePoolSize時(shí),非核心線程在超時(shí)時(shí)間沒有使用的話,是需要退出的;
allowCoreThreadTimeOut是可以通過以下方式進(jìn)行設(shè)置的:
如果沒有進(jìn)行設(shè)置,那么corePoolSize數(shù)量的核心線程會一直存在。
總結(jié)一下:ThreadPoolExecutor內(nèi)部的核心線程如何確保一直存在,不退出?
上面分析已經(jīng)回答了這個(gè)問題,每個(gè)線程在執(zhí)行時(shí)會執(zhí)行runWorker(),而在runWorker()內(nèi)部有while()循環(huán)會判斷getTask(),在getTask()內(nèi)部會對當(dāng)前執(zhí)行的線程數(shù)量及allowCoreThreadTimeOut進(jìn)行實(shí)時(shí)判斷,如果工作數(shù)量大于corePoolSize且workQueue中沒有未執(zhí)行的線程時(shí),會執(zhí)行poll()超時(shí)退出;如果工作數(shù)量不大于corePoolSize且workQueue中沒有未執(zhí)行的線程時(shí),會執(zhí)行take()進(jìn)行阻塞,確保有corePoolSize數(shù)量的線程阻塞在runWorker()內(nèi)部的while()循環(huán)不退出。
如果需要關(guān)閉線程池,需要如何操作呢,看一下shutdown()方法:
以上可以看到,關(guān)閉線程池的原理:a. 遍歷線程池中的所有工作線程;b. 逐個(gè)調(diào)用線程的interrupt()中斷線程(注:無法響應(yīng)中斷的任務(wù)可能永遠(yuǎn)無法終止)
也可調(diào)用shutdownNow()來關(guān)閉線程池,二者區(qū)別:
shutdown():設(shè)置線程池的狀態(tài)為SHUTDOWN,然后中斷所有沒有正在執(zhí)行任務(wù)的線程;
shutdownNow():設(shè)置線程池的狀態(tài)為STOP,然后嘗試停止所有的正在執(zhí)行或暫停任務(wù)的線程,并返回等待執(zhí)行任務(wù)的列表;
使用建議:一般調(diào)用shutdown()關(guān)閉線程池;若任務(wù)不一定要執(zhí)行完,則調(diào)用shutdownNow();
總結(jié)一下:ThreadPoolExecutor在執(zhí)行execute()及shutdown()時(shí)的調(diào)用關(guān)系,流程圖如下:
線程池可以通過Executors來進(jìn)行不同類型的創(chuàng)建,具體分為四種不同的類型,如下:
可緩存線程池:不固定線程數(shù)量,且支持最大為Integer.MAX_VALUE的線程數(shù)量:
1、線程數(shù)無限制
2、有空閑線程則復(fù)用空閑線程,若無空閑線程則新建線程
3、一定程度上減少頻繁創(chuàng)建/銷毀線程,減少系統(tǒng)開銷
固定線程數(shù)量的線程池:定長線程池
1、可控制線程最大并發(fā)數(shù)(同時(shí)執(zhí)行的線程數(shù))
2、超出的線程會在隊(duì)列中等待。
單線程化的線程池:可以理解為線程數(shù)量為1的FixedThreadPool
1、有且僅有一個(gè)工作線程執(zhí)行任務(wù)
2、所有任務(wù)按照指定順序執(zhí)行,即遵循隊(duì)列的入隊(duì)出隊(duì)規(guī)則
定時(shí)以指定周期循環(huán)執(zhí)行任務(wù)
一般來說,等待隊(duì)列 BlockingQueue 有: ArrayBlockingQueue 、 LinkedBlockingQueue 與 SynchronousQueue 。
假設(shè)向線程池提交任務(wù)時(shí),核心線程都被占用的情況下:
ArrayBlockingQueue :基于數(shù)組的阻塞隊(duì)列,初始化需要指定固定大小。
當(dāng)使用此隊(duì)列時(shí),向線程池提交任務(wù),會首先加入到等待隊(duì)列中,當(dāng)?shù)却?duì)列滿了之后,再次提交任務(wù),嘗試加入隊(duì)列就會失敗,這時(shí)就會檢查如果當(dāng)前線程池中的線程數(shù)未達(dá)到最大線程,則會新建線程執(zhí)行新提交的任務(wù)。所以最終可能出現(xiàn)后提交的任務(wù)先執(zhí)行,而先提交的任務(wù)一直在等待。
LinkedBlockingQueue :基于鏈表實(shí)現(xiàn)的阻塞隊(duì)列,初始化可以指定大小,也可以不指定。
當(dāng)指定大小后,行為就和 ArrayBlockingQueue一致。而如果未指定大小,則會使用默認(rèn)的 Integer.MAX_VALUE 作為隊(duì)列大小。這時(shí)候就會出現(xiàn)線程池的最大線程數(shù)參數(shù)無用,因?yàn)闊o論如何,向線程池提交任務(wù)加入等待隊(duì)列都會成功。最終意味著所有任務(wù)都是在核心線程執(zhí)行。如果核心線程一直被占,那就一直等待。
SynchronousQueue :無容量的隊(duì)列。
使用此隊(duì)列意味著希望獲得最大并發(fā)量。因?yàn)闊o論如何,向線程池提交任務(wù),往隊(duì)列提交任務(wù)都會失敗。而失敗后如果沒有空閑的非核心線程,就會檢查如果當(dāng)前線程池中的線程數(shù)未達(dá)到最大線程,則會新建線程執(zhí)行新提交的任務(wù)。完全沒有任何等待,唯一制約它的就是最大線程數(shù)的個(gè)數(shù)。因此一般配合Integer.MAX_VALUE就實(shí)現(xiàn)了真正的無等待。
但是需要注意的是, 進(jìn)程的內(nèi)存是存在限制的,而每一個(gè)線程都需要分配一定的內(nèi)存。所以線程并不能無限個(gè)。
異步通信機(jī)制,將工作線程中需更新UI的操作信息 傳遞到 UI主線程,從而實(shí)現(xiàn) 工作線程對UI的更新處理,最終實(shí)現(xiàn)異步消息的處理。Handler不僅僅能將子線程的數(shù)據(jù)傳遞給主線程,它能實(shí)現(xiàn)任意兩個(gè)線程的數(shù)據(jù)傳遞。
(1)Message
Message 可以在線程之間傳遞消息。可以在它的內(nèi)部攜帶少量數(shù)據(jù),用于在不同線程之間進(jìn)行數(shù)據(jù)交換。除了 what 字段,還可以使用 arg1 和 arg2 來攜帶整型數(shù)據(jù),使用 obj 來攜帶 Object 數(shù)據(jù)。
(2) Handler
Handler 作為處理中心,用于發(fā)送(sendMessage 系列方法)與處理消息(handleMessage 方法)。
(3) MessageQueue
MessageQueue 用于存放所有通過 Handler 發(fā)送的消息。這部分消息會一直存放在消息隊(duì)列中,直到被處理。每個(gè)線程中只會有一個(gè) MessageQueue 對象
(4) Looper
Looper 用于管理 MessageQueue 隊(duì)列,Looper對象通過loop()方法開啟了一個(gè)死循環(huán)——for (;;){},不斷地從looper內(nèi)的MessageQueue中取出Message,并傳遞到 Handler 的 handleMessage() 方法中。每個(gè)線程中只會有一個(gè) Looper 對象。
AsyncTask 是一種輕量級的任務(wù)異步類,可以在后臺子線程執(zhí)行任務(wù),且將執(zhí)行進(jìn)度及執(zhí)行結(jié)果傳遞給 UI 線程。
(1)onPreExecute()
在 UI 線程上工作,在任務(wù)執(zhí)行 doInBackground() 之前調(diào)用。此步驟通常用于設(shè)置任務(wù),例如在用戶界面中顯示進(jìn)度條。
(2)doInBackground(Params... params)
在子線程中工作,在 onPreExecute() 方法結(jié)束后執(zhí)行,這一步被用于在后臺執(zhí)行長時(shí)間的任務(wù),Params 參數(shù)通過 execute(Params) 方法被傳遞到此方法中。任務(wù)執(zhí)行結(jié)束后,將結(jié)果傳遞給 onPostExecute(Result) 方法,同時(shí)我們可以通過 publishProgress(Progress) 方法,將執(zhí)行進(jìn)度發(fā)送給 onProgressUpdate(Progress) 方法。
(3)onProgressUpdate(Progress... values)
在 UI 線程上工作,會在 doInBackground() 中調(diào)用 publishProgress(Progress) 方法后執(zhí)行,此方法用于在后臺計(jì)算仍在執(zhí)行時(shí)(也就是 doInBackgound() 還在執(zhí)行時(shí))將計(jì)算執(zhí)行進(jìn)度通過 UI 顯示出來。例如,可以通過動(dòng)畫進(jìn)度條或顯示文本字段中的日志,從而方便用戶知道后臺任務(wù)執(zhí)行的進(jìn)度。
(4)onPostExecute(Result result)
在 UI 線程上工作,在任務(wù)執(zhí)行完畢(即 doInBackground(Result) 執(zhí)行完畢)并將執(zhí)行結(jié)果傳過來的時(shí)候工作。
使用規(guī)則:
(1)AsyncTask 是個(gè)抽象類,所以要?jiǎng)?chuàng)建它的子類實(shí)現(xiàn)抽象方法
(1)AsyncTask 類必須是在 UI 線程中被加載,但在Android 4.1(API 16)開始,就能被自動(dòng)加載完成。
(2)AsyncTask 類的實(shí)例對象必須在 UI 線程中被創(chuàng)建。
(3)execute() 方法必須是在 UI 線程中被調(diào)用。
(4)不要手動(dòng)調(diào)用方法 onPreExecute()、onPostExecute()、doInBackground()、onProgressUpdate()
(5)任務(wù)只能執(zhí)行一次(如果嘗試第二次執(zhí)行,將拋出異常)。即一個(gè)AsyncTask對象只能調(diào)用一次execute()方法。
原理:
? ? ? 其源碼中原理還是 Thread 與 Handler 的實(shí)現(xiàn),其包含 兩個(gè)線程池,一個(gè) Handler,如下所示:
名稱類型作用
SERIAL_EXECUTOR線程池分發(fā)任務(wù),串行分發(fā),一次只分發(fā)一個(gè)任務(wù)
THREAD_POOL_EXECUTOR線程池執(zhí)行任務(wù),并行執(zhí)行,執(zhí)行的任務(wù)由 SERIAL_EXECUTOR 分發(fā)
InternalHandlerHandler負(fù)責(zé)子線程與主線程的溝通,通知主線程做 UI 工作
一方面減少了每個(gè)并行任務(wù)獨(dú)自建立線程的開銷,另一方面可以管理多個(gè)并發(fā)線程的公共資源,從而提高了多線程的效率。所以ThreadPoolExecutor比較適合一組任務(wù)的執(zhí)行。Executors利用工廠模式對ThreadPoolExecutor進(jìn)行了封裝。
Executors提供了四種創(chuàng)建ExecutorService的方法,他們的使用場景如下:
1. Executors.newFixedThreadPool()
創(chuàng)建一個(gè)定長的線程池,每提交一個(gè)任務(wù)就創(chuàng)建一個(gè)線程,直到達(dá)到池的最大長度,這時(shí)線程池會保持長度不再變化。
當(dāng)線程處于空閑狀態(tài)時(shí),它們并不會被回收,除非線程池被關(guān)閉。當(dāng)所有的線程都處于活動(dòng)狀態(tài)時(shí),新任務(wù)都會處于等待狀態(tài),直到有線程空閑出來。
只有核心線程并且不會被回收,能夠更加快速的響應(yīng)外界的請求。
2. Executors.newCachedThreadPool()
創(chuàng)建一個(gè)可緩存的線程池,如果當(dāng)前線程池的長度超過了處理的需要時(shí),它可以靈活的回收空閑的線程,當(dāng)需要增加時(shí),它可以靈活的添加新的線程,而不會對池的長度作任何限制
線程數(shù)量不定的線程池,只有非核心線程,最大線程數(shù)為 Integer.MAX_VALUE。當(dāng)線程池中的線程都處于活動(dòng)狀態(tài)時(shí),線程池會創(chuàng)建新的線程來處理新任務(wù),否則利用空閑的線程來處理新任務(wù)。線程池中的空閑線程具有超時(shí)機(jī)制,為 60s。
任務(wù)隊(duì)列相當(dāng)于一個(gè)空集合,導(dǎo)致任何任務(wù)都會立即被執(zhí)行,適合執(zhí)行大量耗時(shí)較少的任務(wù)。當(dāng)整個(gè)線程池都處于限制狀態(tài)時(shí),線程池中的線程都會超時(shí)而被停止。
3. Executors.newScheduledThreadPool()
創(chuàng)建一個(gè)定長的線程池,而且支持定時(shí)的以及周期性的任務(wù)執(zhí)行,類似于Timer。
非核心線程數(shù)沒有限制,并且非核心線程閑置的時(shí)候立即回收,主要用于執(zhí)行定時(shí)任務(wù)和具有固定周期的重復(fù)任務(wù)。
4. Executors.newSingleThreadExecutor()
創(chuàng)建一個(gè)單線程化的executor,它只創(chuàng)建唯一的worker線程來執(zhí)行任務(wù)
只有一個(gè)核心線程,保證所有的任務(wù)都在一個(gè)線程中順序執(zhí)行,意義在于不需要處理線程同步的問題。
一般用于執(zhí)行后臺耗時(shí)任務(wù),當(dāng)任務(wù)執(zhí)行完成會自動(dòng)停止;同時(shí)由于它是一個(gè)服務(wù),優(yōu)先級要遠(yuǎn)遠(yuǎn)高于線程,更不容易被系統(tǒng)殺死,因此比較適合執(zhí)行一些高優(yōu)先級的后臺任務(wù)。
使用步驟:創(chuàng)建IntentService的子類,重寫onHandleIntent方法,在onHandleIntent中執(zhí)行耗時(shí)任務(wù)
原理:在源碼實(shí)現(xiàn)上,IntentService封裝了HandlerThread和Handler。onHandleIntent方法結(jié)束后會調(diào)用IntentService的stopSelf(int startId)方法嘗試停止服務(wù)。
IntentService的內(nèi)部是通過消息的方式請求HandlerThread執(zhí)行任務(wù),HandlerThread內(nèi)部又是一種使用Handler的Thread,這就意味著IntentService和Looper一樣是順序執(zhí)行后臺任務(wù)的
(HandlerThread:封裝了Handler + ThreadHandlerThread適合在有需要一個(gè)工作線程(非UI線程)+任務(wù)的等待隊(duì)列的形式,優(yōu)點(diǎn)是不會有堵塞,減少了對性能的消耗,缺點(diǎn)是不能同時(shí)進(jìn)行多個(gè)任務(wù)的處理,需要等待進(jìn)行處理。處理效率低,可以當(dāng)成一個(gè)輕量級的線程池來用)