android 多線程性能優化

基礎概念

CPU核心數和線程數的關係

多核心

指單芯片多處理器( Chip Multiprocessors,簡稱CMP),CMP是由美國斯坦福大學提出的,其思想是將大規模並行處理器中的SMP(對稱多處理器)集成到同一芯片內,各個處理器並行執行不同的進程。這種依靠多個CPU同時並行地運行程序是實現超高速計算的一個重要方向,稱爲並行處理。

多線程

Simultaneous Multithreading.簡稱SMT.SMT可通過複製處理器上的結構狀態,讓同一個處理器上的多個線程同步執行並共享處理器的執行資源可最大限度地實現寬發射、亂序的超標量處理,提高處理器運算部件的利用率,緩和由於數據相關或 Cache未命中帶來的訪問內存延時。

核心數、線程數

目前主流CPU有雙核、三核和四核,六核,八核。增加核心數目就是爲了增加線程數,因爲操作系統是通過線程來執行任務的,一般情況下它們是1:1對應關係,也就是說四核CPU一般擁有四個線程。但 Intel引入超線程技術後,使核心數與線程數形成1:2的關係.

CPU時間片輪轉機制

時間片輪轉調度是一種最古老、最簡單、最公平且使用最廣的算法,又稱RR調度。每個進程被分配一個時間段,稱作它的時間片,即該進程允許運行的時間。
如果在時間片結束時進程還在運行,則CPU將被剝奪並分配給另一個進程。如果進程在時間片結束前阻塞或結來,則CPU當即進行切換。調度程序所要做的就是維護一張就緒進程列表,當進程用完它的時間片後,它被移到隊列的末尾
時間片輪轉調度中唯一有趣的一點是時間片的長度。從一個進程切換到另一個進程是需要定時間的,包括保存和裝入寄存器值及內存映像,更新各種表格和隊列等。假如進程切( processwitch),有時稱爲上下文切換( context switch),需要5ms,再假設時間片設爲20ms,則在做完20ms有用的工作之後,CPU將花費5ms來進行進程切換。CPU時間的20%被浪費在了管理開銷上了。
結論: 時間片設得太短會導致過多的進程切換,降低了CPU效率:而設得太長又可能引起對短的交互請求的響應變差。將時間片設爲100ms通常是一個比較合理的折衷。

進程和線程

進程

程序運行資源分配的最小單位。
進程是操作系統進行資源分配的最小單位,其中資源包括:CPU、內存空間、磁盤10等,同一進程中的多條線程共享該進程中的全部系統資源,而進程和進程之間是相互獨立的。進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。進程是程序在計算機上的一次執行活動。當你運行一個程序,你就啓動了一個進程。
顯然,程序是死的、靜態的,進程是活的、動態的。進程可以分爲系統進程和用戶進程。凡是用於完成操作系統的各種功能的進程就是系統進程,它們就是處於運行狀態下的操作系統本身,用戶進程就是所有由你啓動的進程。

線程

CPU調度的最小單位,必須依賴於進程而存在。
線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的、能獨立運行的基本單位。線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。
線程無處不在,任何一個程序都必須要創建線程,特別是Java不管任何程序都必須啓動一個main函數的主線程; Java Web開發裏面的定時任務、定時器、JSP和 Servlet、異步消息處理機制,遠程訪問接口RM等,任何一個監聽事件, onclick的觸發事件等都離不開線程和併發的知識。

並行和併發

併發

指應用能夠交替執行不同的任務,比如單CPU核心下執行多線程並非是同時執行多個任務,如果你開兩個線程執行,就是在你幾乎不可能察覺到的速度不斷去切換這兩個任務,已達到"同時執行效果",其實並不是的,只是計算機的速度太快,我們無法察覺到而已.

並行

指應用能夠同時執行不同的任務,例:吃飯的時候可以邊吃飯邊打電話,這兩件事情可以同時執行
兩者區別: 一個是交替執行,一個是同時執行.

高併發編程

優點

  1. 充分利用CPU的資源
    從上面的CPU的介紹,可以看的出來,現在市面上沒有CPU的內核不使用多線程併發機制的,特別是服務器還不止一個CPU,如果還是使用單線程的技術做思路,明顯就out了。因爲程序的基本調度單元是線程,並且一個線程也只能在一個CPU的一個核的一個線程跑,如果你是個i3的CPU的話,最差也是雙核心4線程的運算能力:如果是一個線程的程序的話,那是要浪費3/4的CPU性能:如果設計一個多線程的程序的話,那它就可以同時在多個CPU的多個核的多個線程上跑,可以充分地利用CPU,減少CPU的空閒時間,發揮它的運算能力,提高併發量。
  2. 加快響應用戶的時間
    在做程序開發的時候更應該如此,特別是我們做互聯網項目,網頁的響應時間若提升1s,如果流量大的話,就能增加不少轉換量。做過高性能web前端調優的都知道,要將靜態資源地址用兩三個子域名去加載,爲什麼?因爲每多一個子域名,瀏覽器在加載你的頁面的時候就會多開幾個線程去加載你的頁面資源,提升網站的響應速度。多線程,高併發真的是無處不在。
  3. 可以使你的代碼模塊化,異步化,簡單化
    例如我們在做 Android程序開發的時候,主線程的UI展示部分是一塊主代碼程序部分,但是UI上的按鈕用相應事件的處理程序就可以做個單獨的模塊程序拿出來。這樣既增加了異步的操,又使程序模塊化,清晰化和簡單化。

注意事項

  1. 線程之間的安全性
    從前面的章節中我們都知道,在同一個進程裏面的多線程是資源共享的,也就是都可以訪問同一個內存地址當中的一個變量。例如:若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的:若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。
  2. 線程之間的死循環過程
    爲了解決線程之間的安全性引入了Java的鎖機制,而一不小心就會產生Java線程死鎖的多線程問題,因爲不同的線程都在等待那些根本不可能被釋放的鎖,從而導致所有的工作都無法完成。假設有兩個線程,分別代表兩個飢餓的人,他們必須共享刀叉並輪流吃飯。他們都需要獲得兩個鎖:共享刀和共享叉的鎖。
    假如線程A獲得了刀,而線程B獲得了叉。線程A就會進入阻塞狀態來等待獲得叉,而線程B則阻塞來等待線程A所擁有的刀。這只是人爲設計的例子,但儘管在運行時很難探測到,這類情況卻時常發生。
  3. 線程太多了會將服務器資源耗盡形成死機當機
    線程數太多有可能造成系統創建大量線程而導致消耗完系統內存以及CPU的“過渡切換”,造成系統的死機。某些系統資源是有限的,如文件描述符。多線程程序可能耗盡資源,因爲每個線程都可能希望有一個這樣的資源。如果線程數相當大,或者某個資源的侯選線程數遠遠超過了可用的資源數則最好使用資源池。一個最好的示例是數據庫連接池。只要線程需要使用一個數據庫連接,它就從池中取出一個,使用以後再將它返回池中。資源池也稱爲資源庫。

Java裏的線程

Java裏的程序天生就是多線程的。
一個Java程序從main()方法開始執行,然後按照既定的代碼邏輯執行,看似沒有其他線程參與,但實際上Java程序天生就是多線程程序,因爲執行main()方法的是一個名稱爲main的線程。

  1. main //main線程,用戶程序入口
  2. Reference Handler//清除Reference的線程
  3. Finalizer // 調用對象finalize方法的線程
  4. Signal Dispatcher // 分發處理髮送給JVM信號的線程
  5. Attach Listener //內存dump,線程dump,類信息統計,獲取系統屬性等
  6. Monitor Ctrl-Break //監控Ctrl-Break中斷信號的

線程的啓動與中止

啓動

  1. X extends Thread;,然後X.run
  2. X implements Runnable;然後交給Thread運行
  3. X implements Callable;然後交給Thread運行

Callable、Future和FutureTask

Runnable是一個接口,在它裏面只聲明瞭一個run()方法,由於run()方法返回值爲void類型,所以在執行完任務之後無法返回任何結果。
Callable是一個接口,在它裏面也只聲明瞭一個方法,只不過這個方法叫做call(),這是一個泛型接口,call()函數返回的類型就是傳遞進來的V類型。
Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過get方法獲取執行結果,該方法會阻塞直到任務返回結果。
因爲Future只是一個接口,所以是無法直接用來創建對象使用的,因此就有了FutureTask。FutureTask類實現了RunnableFuture接口,RunnableFuture繼承了Runnable接口和Future接口,所以它既可以作爲Runnable被線程執行,又可以作爲Future得到Callable的返回值。

中止

線程自然終止: 要麼是run執行完成了,要麼是拋出了一個未處理的異常導致線程提前結束。
手動中止: 暫停、恢復和停止操作對應在線程Thread的API就是suspend()、resume()和stop()。這些API是過期的,也就是不建議使用的。在調用後,線程不會釋放已經佔有的資源(比如鎖),而是佔有着資源進入睡眠狀態,這樣容易引發死鎖問題。終結一個線程時不會保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機會,因此會導致程序可能工作在不確定狀態下。
安全的中止: 其他線程通過調用某個線程A的interrupt()方法對其進行中斷操作, 中斷好比其他線程對該線程打了個招呼,“A,你要中斷了”,不代表線程A會立即停止自己的工作,同樣的A線程完全可以不理會這種中斷請求。因爲java裏的線程是協作式的,不是搶佔式的。線程通過檢查自身的中斷標誌位是否被置爲true來進行響應,線程通過方法isInterrupted()來進行判斷是否被中斷,也可以調用靜態方法Thread.interrupted()來進行判斷當前線程是否被中斷,不過Thread.interrupted()會同時將中斷標識位改寫爲false。
如果一個線程處於了阻塞狀態(如線程調用了thread.sleep、thread.join、thread.wait、),則在線程在檢查中斷標示時如果發現中斷標示爲true,則會在這些阻塞方法調用處拋出InterruptedException異常,並且在拋出異常後會立即將線程的中斷標示位清除,即重新設置爲false。
**不建議自定義一個取消標誌位來中止線程的運行。**因爲run方法裏有阻塞調用時會無法很快檢測到取消標誌,線程必須從阻塞調用返回後,纔會檢查這個取消標誌。這種情況下,使用中斷會更好,因爲,一、一般的阻塞方法,如sleep等本身就支持中斷的檢查,二、檢查中斷位的狀態和檢查取消標誌位沒什麼區別,用中斷位的狀態還可以避免聲明取消標誌位,減少資源的消耗。
注意: 處於死鎖狀態的線程無法被中斷。

run()和start()

**start()**方法讓一個線程進入就緒隊列等待分配cpu,分到cpu後才調用實現的run()方法,start()方法不能重複調用。
**run()**方法是業務邏輯實現的地方,本質上和任意一個類的任意一個成員方法並沒有任何區別,可以重複執行,可以被單獨調用。

yield()和join()

yield(): 使當前線程讓出CPU佔有權,但讓出的時間是不可設定的。也不會釋放鎖資源,所有執行yield()的線程有可能在進入到可執行狀態後馬上又被執行。
join(): 把指定的線程加入到當前線程,可以將兩個交替執行的線程合併爲順序執行的線程。比如在線程B中調用了線程A的Join()方法,直到線程A執行完畢後,纔會繼續執行線程B。

wait()/notify()/notifyAll()

notify(): 通知一個在對象上等待的線程,使其從wait方法返回,而返回的前提是該線程獲取到了對象的鎖,沒有獲得鎖的線程重新進入WAITING狀態。
notifyAll(): 通知所有等待在該對象上的線程。
wait() 調用該方法的線程進入 WAITING狀態,只有等待另外線程的通知或被中斷纔會返回.需要注意,調用wait()方法後,會釋放對象的鎖。
wait(long) 超時等待一段時間,這裏的參數時間是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回。
wait (long,int) 對於超時時間更細粒度的控制,可以達到納秒。

線程間的共享

Java支持多個線程同時訪問一個對象或者對象的成員變量,關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性,又稱爲內置鎖機制。
對象鎖和類鎖:
對象鎖是用於對象實例方法,或者一個對象實例上的,類鎖是用於類的靜態方法或者一個類的class對象上的。我們知道,類的對象實例可以有很多個,但是每個類只有一個class對象,所以不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖。類鎖只是一個概念上的東西,並不是真實存在的,類鎖其實鎖的是每個類的對應的class對象。類鎖和對象鎖之間也是互不干擾的。

線程間的協作

線程之間相互配合,完成某項工作,比如:一個線程修改了一個對象的值,而另一個線程感知到了變化,然後進行相應的操作,整個過程開始於一個線程,而最終執行又是另一個線程。前者是生產者,後者就是消費者,這種模式隔離了“做什麼”(what)和“怎麼做”(How),簡單的辦法是讓消費者線程不斷地循環檢查變量是否符合預期在while循環中設置不滿足的條件,如果條件滿足則退出while循環,從而完成消費者的工作。卻存在如下問題:難以確保及時性。難以降低開銷。

等待/通知機制

一個線程A調用了對象O的wait()方法進入等待狀態,而另一個線程B調用了對象O的notify()或者notifyAll()方法,線程A收到通知後從對象O的wait()方法返回,進而執行後續操作。上述兩個線程通過對象O來完成交互,而對象上的wait()和notify/notifyAll()的關係就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。
等待方遵循如下原則:

  1. 獲取對象的鎖。
  2. 使用while循環來判斷,如果條件不滿足,那麼調用對象的wait()方法,被通知後仍要檢查條件。
  3. 條件滿足則執行對應的邏輯。
    通知方遵循如下原則:
  4. 獲得對象的鎖。
  5. 改變條件。
  6. 通知所有等待在對象上的線程。
    在調用wait()之前,線程必須要獲得該對象的對象級別鎖,即只能在同步方法或同步塊中(synchronized)調用wait()方法,進入wait()方法後,當前線程釋放鎖,在從wait()返回前,線程與其他線程競爭重新獲得鎖。notifyAll()方法一旦該對象鎖被釋放(退出調用了notifyAll()的synchronized代碼塊的時候),他們就會去競爭。如果其中一個線程獲得了該對象鎖,它就會繼續往下執行,在它退出synchronized代碼塊,釋放鎖後,其他的已經被喚醒的線程將會繼續競爭獲取該鎖,一直進行下去,直到所有被喚醒的線程都執行完畢。
    儘可能用notifyall(),謹慎使用notify()。

ThreadLocal

線程變量,是一個以ThreadLocal對象爲鍵、任意對象爲值的存儲結構。這個結構被附帶在線程上,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值, ThreadLocal往往用來實現變量在線程之間的隔離。(例如一個線程只有一個Looper,詳見Handler源碼解析

隱式鎖

我們一般的Java程序是靠synchronized關鍵字實現鎖功能的,使用synchronized關鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放。synchronized屬於Java語言層面的鎖,也被稱之爲內置鎖。
synchronized這種機制,一旦開始獲取鎖,是不能中斷的,也不提供嘗試獲取鎖的機制。

顯式鎖

Lock是由Java在語法層面提供的,鎖的獲取和釋放需要我們明顯的去獲取,因此被稱爲顯式鎖。並且提供了synchronized不提供的機制。

特性 描述
嘗試非阻塞的獲取鎖 當前線程嘗試獲取鎖,如果這一時刻沒有被其他線程獲取,則成功
能被中斷的獲取鎖 獲取到鎖的線程能夠響應中斷,當獲取到鎖的線程被中斷,拋出中斷異常,同時釋放鎖
超時獲取鎖 再指定的截至時間前獲取鎖,如果時間到了還沒有獲取到,則返回

Lock一定要在finally塊中釋放鎖,目的是保證在獲取到鎖之後,最終能夠被釋放。

可重入鎖

ReentrantLock,在調用lock()方法時,已經獲取到鎖的線程,能夠再次調用lock()方法獲取鎖而不被阻塞。
synchronized關鍵字隱式的支持重進入,比如一個synchronized修飾的遞歸方法,在方法執行時,執行線程在獲取了鎖之後仍能連續多次地獲得該鎖。

公平和非公平鎖

如果在時間上,先對鎖進行獲取的請求一定先被滿足,那麼這個鎖是公平的,反之,是不公平的。公平的獲取鎖,也就是等待時間最長的線程最優先獲取鎖,也可以說鎖獲取是順序的。
ReentrantLock提供了一個構造函數,能夠控制鎖是否是公平的。事實上,公平的鎖機制往往沒有非公平的效率高。原因是,在恢復一個被掛起的線程與該線程真正開始運行之間存在着嚴重的延遲。

讀寫鎖

ReentrantReadWriteLock 在同一時刻可以允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀鎖和寫鎖,使得併發性相比一般的排他鎖有了很大提升
synchronized和ReentrantLock基本都是排他鎖,這些鎖在同一時刻只允許一個線程進行訪問。

Condition

Lock用Condition實現等待通知。
任意一個Java對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。Condition接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式。

CAS

原子操作
假定有兩個操作A和B,如果從執行A的線程來看,當另一個線程執行B時,要麼將B全部執行完,要麼完全不執行B,那麼A和B對彼此來說是原子的。
實現原子操作
實現原子操作可以使用鎖,鎖機制,滿足基本的需求是沒有問題的了,但是有的時候我們的需求並非這麼簡單,我們需要更有效,更加靈活的機制,synchronized關鍵字是基於阻塞的鎖機制,也就是說當一個線程擁有鎖的時候,訪問同一資源的其它線程需要等待,直到該線程釋放鎖,這裏會有些問題:首先,如果被阻塞的線程優先級很高很重要怎麼辦?其次,如果獲得鎖的線程一直不釋放鎖怎麼辦?(這種情況是非常糟糕的)。還有一種情況,如果有大量的線程來競爭資源,那CPU將會花費大量的時間和資源來處理這些競爭(事實上CPU的主要工作並非這些),同時,還有可能出現一些例如死鎖之類的情況,最後,其實鎖機制是一種比較粗糙,粒度比較大的機制,相對於像計數器這樣的需求有點兒過於笨重。
CAS
實現原子操作還可以使用當前的處理器基本都支持CAS()的指令,只不過每個廠家所實現的算法並不一樣罷了,每一個CAS操作過程都包含三個運算符:一個內存地址V,一個期望的值A和一個新值B,操作的時候如果這個地址上存放的值等於這個期望的值A,則將地址上的值賦爲新值B,否則不做任何操作。
CAS的基本思路就是,如果這個地址上的值和期望的值相等,則給其賦予新值,否則不做任何事兒,但是要返回原值是多少。循環CAS就是在一個循環裏不斷的做CAS操作,直到成功爲止。怎麼實現線程安全呢?語言層面不做處理,我們將其交給硬件—CPU和內存,利用CPU的多處理能力,實現硬件層面的阻塞,再加上volatile變量的特性即可實現基於原子操作的線程安全。
三大問題

  1. ABA問題。
    因爲CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A。
  2. 循環時間長開銷大。
    自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。
  3. 只能保證一個共享變量的原子操作。
    當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖。還有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如,有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象裏來進行CAS操作。

線程池原理

線程池

將線程進行池化,需要運行任務時從池中拿一個線程來執行,執行完畢,線程放回池中。優點:

  1. 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  2. 提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
  3. 提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。

JDK中的線程池

public ThreadPoolExecutor(int corePoolSize,
						  int maximumPoolSize,
						  long keepAliveTime,
						  TimeUnit unit,
						  BlockingQueue<Runnable> workQueue,
						  ThreadFactory threadFactory,
						  RejectedExecutionHandler handler)

corePoolSize

線程池中的核心線程數,當提交一個任務時,線程池創建一個新線程執行任務,直到當前線程數等於corePoolSize。
如果當前線程數爲corePoolSize,繼續提交的任務被保存到阻塞隊列中,等待被執行。
如果執行了線程池的prestartAllCoreThreads()方法,線程池會提前創建並啓動所有核心線程。

maximumPoolSize

線程池中允許的最大線程數。如果當前阻塞隊列滿了,且繼續提交任務,則創建新的線程執行任務,前提是當前線程數小於maximumPoolSize。

keepAliveTime

線程空閒時的存活時間,即當線程沒有任務執行時,繼續存活的時間。默認情況下,該參數只在線程數大於corePoolSize時纔有用。

TimeUnit

keepAliveTime的時間單位。

workQueue

workQueue必須是BlockingQueue阻塞隊列。當線程池中的線程數超過它的corePoolSize的時候,線程會進入阻塞隊列進行阻塞等待。通過workQueue,線程池實現了阻塞功能

threadFactory

創建線程的工廠,通過自定義的線程工廠可以給每個新建的線程設置一個具有識別度的線程名Executors靜態工廠裏默認的threadFactory,線程的命名規則是“pool-數字-thread-數字”。

RejectedExecutionHandler

飽和策略,當阻塞隊列滿了,且沒有空閒的工作線程,如果繼續提交任務,必須採取一種策略處理該任務,線程池提供了4種策略:

  1. AbortPolicy:直接拋出異常,默認策略;
  2. CallerRunsPolicy:用調用者所在的線程來執行任務;
  3. DiscardOldestPolicy:丟棄阻塞隊列中靠最前的任務,並執行當前任務;
  4. DiscardPolicy:直接丟棄任務;

也可以根據應用場景實現RejectedExecutionHandler接口,自定義飽和策略,如記錄日誌或持久化存儲不能處理的任務。

阻塞隊列

隊列

隊列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。進行插入操作的端稱爲隊尾,進行刪除操作的端稱爲隊頭。隊列中沒有元素時,稱爲空隊列。
隊列的數據元素又稱爲隊列元素。在隊列中插入一個隊列元素稱爲入隊,從隊列中刪除一個隊列元素稱爲出隊。因爲隊列只允許在一端插入,在另一端刪除,所以只有最早進入隊列的元素才能最先從隊列中刪除,故隊列又稱爲先進先出(FIFO—first in first out)線性表。

阻塞隊列

  1. 支持阻塞的插入方法:意思是當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。
  2. 支持阻塞的移除方法:意思是在隊列爲空時,獲取元素的線程會等待隊列變爲非空。

阻塞隊列常用於生產者和消費者的場景,生產者是向隊列裏添加元素的線程,消費者是從隊列裏取元素的線程。阻塞隊列就是生產者用來存放元素、消費者用來獲取元素的容器。
常用阻塞隊列
ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列。
LinkedBlockingQueue:一個由鏈表結構組成的有界阻塞隊列。
PriorityBlockingQueue:一個支持優先級排序的無界阻塞隊列。
DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。
LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。

線程池的工作機制

  1. 如果當前運行的線程少於corePoolSize,則創建新線程來執行任務(注意,執行這一步驟需要獲取全局鎖)。
  2. 如果運行的線程等於或多於corePoolSize,則將任務加入BlockingQueue。
  3. 如果無法將任務加入BlockingQueue(隊列已滿),則創建新的線程來處理任務(注意,執行這一步驟需要獲取全局鎖)。
  4. 如果創建新線程將使當前運行的線程超出maximumPoolSize,任務將被拒絕,並調用RejectedExecutionHandler.rejectedExecution()方法。

配置線程池

要想合理地配置線程池,就必須首先分析任務特性,可以從以下幾個角度來分析。

  1. 任務的性質:CPU密集型任務、IO密集型任務和混合型任務。
  2. 任務的優先級:高、中和低。
  3. 任務的執行時間:長、中和短。
  4. 任務的依賴性:是否依賴其他系統資源,如數據庫連接。

性質不同的任務可以用不同規模的線程池分開處理。CPU密集型任務應配置儘可能小的線程,如配置cpu+1個線程的線程池。由於IO密集型任務線程並不是一直在執行任務,則應配置儘可能多的線程,如2*cpu。混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於串行執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行分解。
可以通過 Runtime.getRuntime().availableProcessors() 方法獲得當前設備的CPU個數。

AsyncTask

在Android當中,通常將線程分爲兩種,一種叫做Main Thread,除了Main Thread之外的線程都可稱爲Worker Thread。

  1. 絕對不能在UI Thread當中進行耗時的操作,不能阻塞我們的UI Thread
  2. 不能在UI Thread之外的線程當中操縱我們的UI元素

AsyncTask是個abstract類,所以在使用時需要實現一個AsyncTask的具體實現類,一般來說會覆蓋4個方法

  1. onPreExecute():在執行後臺下載操作之前調用,將下載等待動畫顯示出來,運行在主線程中。
  2. doInBackground():核心方法,執行後臺下載操作的方法,必須實現的一個方法,運行在子線程中;這個方法是執行在子線程中的。在onPreExecute()執行完後,會立即開啓這個方法。
  3. onProgressUpdate():在下載操作doInBackground()中調用publishProgress()時的回調方法,用於更新下載進度,運行在主線程中。
  4. onPostExecute():後臺下載操作完成後調用,將下載等待動畫進行隱藏,並更新UI,運行在主線程中。

構造方法

	public AsyncTask(@Nullable Looper callbackLooper) {
        mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
            ? getMainHandler()
            : new Handler(callbackLooper);
		//Callable接口的封裝,意味着這個任務是有返回值的
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                Result result = null;
                try {
                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                    //noinspection unchecked
                    //運行結果
                    result = doInBackground(mParams);
                    Binder.flushPendingCommands();
                } catch (Throwable tr) {
                    mCancelled.set(true);
                    throw tr;
                } finally {
                	//發送結果
                    postResult(result);
                }
                return result;
            }
        };
		//FutureTask標準用法,mWorker作爲Callable被傳給了mFuture,那麼mFuture的結果就從mWorker執行的任務中取得。
        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occurred while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
    }

執行方法execute()

 @MainThread
    public static void execute(Runnable runnable) {
        sDefaultExecutor.execute(runnable);
    }

默認是SerialExecutor

 	public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
    @UnsupportedAppUsage
    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    private static class SerialExecutor implements Executor {
    	//雙端隊列
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {
        	//從隊尾插入數據
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null) {
                scheduleNext();
            }
        }

        protected synchronized void scheduleNext() {
        	//從隊頭獲取元素
            if ((mActive = mTasks.poll()) != null) {
            	//把所有的任務丟入一個容器,之後把容器裏面的所有對象一個一個的排隊(串行化)執行
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }

這個線程池定義如下

    private static final int CORE_POOL_SIZE = 1;
    private static final int MAXIMUM_POOL_SIZE = 20;
    private static final int BACKUP_POOL_SIZE = 5;
    private static final int KEEP_ALIVE_SECONDS = 3;
public static final Executor THREAD_POOL_EXECUTOR;

static {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(), sThreadFactory);
    threadPoolExecutor.setRejectedExecutionHandler(sRunOnSerialPolicy);
    THREAD_POOL_EXECUTOR = threadPoolExecutor;
}

結果和進度的通知,發送數據的方法

private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
            new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}

和更新進度時我們會調用的publishProgress方法

protected final void publishProgress(Progress... values) {
    if (!isCancelled()) {
        getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskResult<Progress>(this, values)).sendToTarget();
    }
}

實際上使用的是Handler發送數據

private static class InternalHandler extends Handler {
        public InternalHandler(Looper looper) {
            super(looper);
        }

        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }

總結

  1. 線程池的創建:
    在創建了AsyncTask的時候,會默認創建兩個線程池SerialExecutor和ThreadPoolExecutor,SerialExecutor負責將任務串行化,ThreadPoolExecutor是真正執行任務的地方,且無論有多少個AsyncTask實例,兩個線程池都會只有一份。
  2. 任務的執行:
    在execute中,會執行run方法,當執行完run方法後,會調用scheduleNext()不斷的從雙端隊列中輪詢,獲取下一個任務並繼續放到一個子線程中執行,直到異步任務執行完畢。
  3. 消息的處理:
    在執行完onPreExecute()方法之後,執行了doInBackground()方法,然後就不斷的發送請求獲取數據;在這個AsyncTask中維護了一個InternalHandler的類,這個類是繼承Handler的,獲取的數據是通過handler進行處理和發送的。在其handleMessage方法中,將消息傳遞給onProgressUpdate()進行進度的更新,也就可以將結果發送到主線程中,進行界面的更新了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章