在我學習 Android 多線程優化方法的過程當中,發現我對多線程優化的瞭解太片面。html
寫這篇文章的目的是完善我對 Android 多線程優化方法的認識,分享這篇文章的目的是但願你們也能從這些知識從獲得一些啓發。java
這篇文章分爲下面三部分。android
第一部分面試
第一部分講的是多線程優化的基礎知識,包括線程的介紹和線程調度基本原理的介紹。算法
第二部分shell
第二部分講的是多線程優化須要預防的一些問題,包括線程安全問題的介紹和實現線程安全的辦法。編程
第三部分緩存
第三部分講的是多線程優化可使用的一些方法,包括線程之間的協做方式與 Android 執行異步任務的經常使用方式。安全
在閱讀本文時,畫圖和思考能夠幫助你更好地記憶和理解文中的內容。網絡
畫圖
畫圖指的是把每一節的重點畫在思惟導圖的節點上。
思惟導圖可讓隨意信息在視覺上創建起一種視覺上的關聯。
隨意信息指的是不存在邏輯關係的信息,好比線程的名字和線程的狀態就是一種隨意信息。
隨意信息的特色就是它們之間不存在邏輯關聯,致使記憶困難。
經過創建關聯,咱們大腦能更好地記憶隨意信息。
思考
學習不是爲了被現有的知識所束縛,而是以現有的知識爲基石,發展出新的思想。
閱讀本文時,能夠帶着下面這些問題邊思考邊閱讀。
AS
Android Studio(Android 應用開發工具)
GC
ART
Android Runtime(Android 應用運行時環境)
JVM
Java Virtual Machine(Java 虛擬機)
JUC
java.util.concurrent(Java 併發包)
無論你懂不懂多線程,你也必需要用多線程。
GC 線程
假如咱們如今運行的是用 AS 建的一個啥也沒有的 demo 項目,那也不表明咱們運行的是一個單線程應用。
由於這個應用是運行在 ART 上的,而 ART 自帶了 GC 線程,再加上主線程,它依舊是一個多線程應用。
第三方線程
在咱們開發應用的過程當中,即便咱們沒有直接建立線程,也間接地建立了線程。
由於咱們平常使用的第三方庫,包括 Android 系統自己都用到了多線程。
好比 Glide 就是使用工做線程從網絡上加載圖片,等圖片加載完畢後,再切回主線程把圖片設置到 ImageView 中。
硬性要求
假如咱們的應用中只有一個線程,意味着加載圖片時 Loading 動畫沒法播放,界面是卡死的,用戶會失去耐心。
並且 Android 強制要求開發者在發起網絡請求時,必須在工做線程,不能在主線程,也就是開發 Android 應用必須使用多線程。
既然上面說到了使用多線程是不可避免的,那使用多線程又會遇到哪些問題呢?
作多線程優化是爲了解決多線程的安全性和活躍性問題。
這兩個問題會致使多線程程序輸出錯誤的結果以及任務沒法執行,下面咱們就來看看這兩個問題的表現。
安全性問題
假如如今有兩個廚師小張和老王,他們兩我的分別作兩道菜,你們都知道本身的菜放了多少鹽,多少糖,在這種狀況下出問題的機率比較低。
可是若是兩我的作一個菜呢?
小張在作一個菜,作着作着鍋被老王搶走了,老王不知道小張有沒有放鹽,就又放了一次鹽,結果炒出來的菜太鹹了,無法吃,而後他們就決定要出去皇城 PK。
這裏的「菜」對應着咱們程序中的數據。
而這種現象就是致使線程出現安全性的緣由之一:競態(Race Condition)。
之因此會出現競態是由 Java 的內存模型和線程調度機制決定的,關於 Java 的線程調度機制,在後面會有更詳細的介紹。
活躍性問題
自從上次出了皇城 PK 的事情後,經理老李出了一條規定,打架扣 100,這條規定一出,小張和老王不再敢 PK 了,不過沒過幾天,他們就找到了一種新的方式來互懟。
有一天,小張在作菜,小張要先放鹽再放糖,而老王拿着鹽,老王要先放糖再放鹽,結果過了兩個小時兩我的都沒把菜作出來,經理老李再次陷入懵逼的狀態。
這就是線程活躍性問題的現象之一:死鎖(Deadlock)。
關於線程安全性的三個問題和線程活躍性的四個問題,在本文後面會作更詳細的介紹。
上一節咱們講到了多線程編程可能會致使程序出現這樣那樣的問題,那什麼是線程呢?
咱們這一節的內容包括下面幾個部分。
線程是進程中可獨立執行的最小單位,也是 CPU 資源分配的基本單位。
進程是程序向操做系統申請資源的基本條件,一個進程能夠包含多個線程,同一個進程中的線程能夠共享進程中的資源,如內存空間和文件句柄。
操做系統會把資源分配給進程,可是 CPU 資源比較特殊,它是分配給線程的,這裏說的 CPU 資源也就是 CPU 時間片。
進程與線程的關係,就像是飯店與員工的關係,飯店爲顧客提供服務,而提供服務的具體方式是經過一個個員工實現的。
線程的做用是執行特定任務,這個任務能夠是下載文件、加載圖片、繪製界面等。
線程有編號、名字、類別以及優先級四個屬性,除此以外,線程的部分屬性還具備繼承性,下面咱們就來看看線程的四個屬性的做用和線程的繼承性。
做用
線程的編號(id)用於標識不一樣的線程,每條線程擁有不一樣的編號。
注意事項
不能做爲惟一標識
某個編號的線程運行結束後,該編號可能被後續建立的線程使用,所以編號不適合用做惟一標識
只讀
編號是隻讀屬性,不能修改
每一個線程都有本身的名字(name),名字的默認值是 Thread-線程編號,好比 Thread-0 。
除了默認值,咱們也能夠給線程設置名字,以咱們本身的方式去區分每一條線程。
做用
給線程設置名字可讓咱們在某條線程出現問題時,用該線程的名字快速定位出問題的地方
線程的類別(daemon)分爲守護線程和用戶線程,咱們能夠經過 setDaemon(true) 把線程設置爲守護線程。
當 JVM 要退出時,它會考慮是否全部的用戶線程都已經執行完畢,是的話則退出。
而對於守護線程,JVM 在退出時不會考慮它是否執行完成。
做用
守護線程一般用於執行不重要的任務,好比監控其餘線程的運行狀況,GC 線程就是一個守護線程。
注意事項
setDaemon() 要在線程啓動前設置,不然 JVM 會拋出非法線程狀態異常(IllegalThreadStateException)。
做用
線程的優先級(Priority)用於表示應用但願優先運行哪一個線程,線程調度器會根據這個值來決定優先運行哪一個線程。
取值範圍
Java 中線程優先級的取值範圍爲 1~10,默認值是 5,Thread 中定義了下面三個優先級常量。
注意事項
不保證
線程調度器把線程的優先級看成一個參考值,不必定會按咱們設定的優先級順序執行線程
線程飢餓
優先級使用不當會致使某些線程永遠沒法執行,也就是線程飢餓的狀況,關於線程飢餓,在第 7 大節會有更多的介紹
線程的繼承性指的是線程的類別和優先級屬性是會被繼承的,線程的這兩個屬性的初始值由開啓該線程的線程決定。
假如優先級爲 5 的守護線程 A 開啓了線程 B,那麼線程 B 也是一個守護線程,並且優先級也是 5 。
這時咱們就把線程 A 叫作線程 B 的父線程,把線程 B 叫作線程 A 的子線程。
線程的經常使用方法有六個,它們分別是三個非靜態方法 start()、run()、join() 和三個靜態方法 currentThread()、yield()、sleep() 。
下面咱們就來看下這六個方法都有哪些做用和注意事項。
做用
start() 方法的做用是啓動線程。
注意事項
該方法只能調用一次,再次調用不只沒法讓線程再次執行,還會拋出非法線程狀態異常。
做用
run() 方法中放的是任務的具體邏輯,該方法由 JVM 調用,通常狀況下開發者不須要直接調用該方法。
注意事項
若是你調用了 run() 方法,加上 JVM 也調用了一次,那這個方法就會執行兩次
做用
join() 方法用於等待其餘線程執行結束。
若是線程 A 調用了線程 B 的 join() 方法,那線程 A 會進入等待狀態,直到線程 B 運行結束。
注意事項
join() 方法致使的等待狀態是能夠被中斷的,因此調用這個方法須要捕獲中斷異常
做用
currentThread() 方法是一個靜態方法,用於獲取執行當前方法的線程。
咱們能夠在任意方法中調用 Thread.currentThread() 獲取當前線程,並設置它的名字和優先級等屬性。
做用
yield() 方法是一個靜態方法,用於使當前線程放棄對處理器的佔用,至關因而下降線程優先級。
調用該方法就像是是對線程調度器說:「若是其餘線程要處理器資源,那就給它們,不然我繼續用」。
注意事項
該方法不必定會讓線程進入暫停狀態。
做用
sleep(ms) 方法是一個靜態方法,用於使當前線程在指定時間內休眠(暫停)。
線程不止提供了上面的 6 個方法給咱們使用,而其餘方法的使用在文章的後面會有一個更詳細的介紹。
和 Activity 同樣,線程也有本身的生命週期,並且生命週期事件也是由用戶(開發者)觸發的。
從 Activity 的角度來看,用戶點擊按鈕後打開一個 Activity,就至關因而觸發了 Activity 的 onCreate() 方法。
從線程的角度來看,開發者調用了 start() 方法,就至關因而觸發了 Thread 的 run() 方法。
若是咱們在上一個 Activity 的 onPause() 方法中進行了耗時操做,那麼下一個 Activity 的顯示也會由於這個耗時操做而慢一點顯示,這就至關因而 Thread 的等待狀態。
線程的生命週期不只能夠由開發者觸發,還會受到其餘線程的影響,下面是線程各個狀態之間的轉換示意圖。
咱們能夠經過 Thread.getState() 獲取線程的狀態,該方法返回的是一個枚舉類 Thread.State。
線程的狀態有新建、可運行、阻塞、等待、限時等待和終止 6 種,下面咱們就來看看這 6 種狀態之間的轉換過程。
當一個線程建立後未啓動時,它就處於新建(NEW)狀態。
當咱們調用線程的 start() 方法後,線程就進入了可運行(RUNNABLE)狀態。
可運行狀態又分爲預備(READY)和運行(RUNNING)狀態。
預備狀態
處於預備狀態的線程可被線程調度器調度,調度後線程的狀態會從預備轉換爲運行狀態,處於預備狀態的線程也叫活躍線程。
運行狀態
運行狀態表示線程正在運行,也就是處理器正在執行線程的 run() 方法。
當線程的 yield() 方法被調用後,線程的狀態可能由運行狀態變爲預備狀態。
當下面幾種狀況發生時,線程就處於阻塞(BLOCKED)狀態。
一個線程執行特定方法後,會等待其餘線程執行執行完畢,此時線程進入了等待(WAITING)狀態。
等待狀態
下面的幾個方法可讓線程進入等待狀態。
Object.wait()
LockSupport.park()
Thread.join()
可運行狀態
下面的幾個方法可讓線程從等待狀態轉變爲可運行狀態,而這種轉變又叫喚醒。
Object.notify()
Object.notifyAll()
LockSupport.unpark()
限時等待狀態 (TIMED_WAITING)與等待狀態的區別就是,限時等待是等待一段時間,時間到了以後就會轉換爲可運行狀態。
下面的幾個方法可讓線程進入限時等待狀態,下面的方法中的 ms、ns、time 參數分別表明毫秒、納秒以及絕對時間。
當線程的任務執行完畢或者任務執行遇到異常時,線程就處於終止(TERMINATED)狀態。
閱讀完上一節的內容後,咱們對線程有了基本的瞭解,知道了什麼是線程,也知道了線程的生命週期是怎麼流轉的。
這一節咱們就來看看線程是怎麼被調度的,這一節包括如下內容。
瞭解 Java 的內存模型,能幫助咱們更好地理解線程的安全性問題,下面咱們就來看看什麼是 Java 的內存模型。
Java 內存模型(Java Memory Model,JMM)規定了全部變量都存儲在主內存中,每條線程都有本身的工做內存。
JVM 把內存劃分紅了好幾塊,其中方法區和堆內存區域是線程共享的。
假如如今有三個線程同時對值爲 5 的變量 a 進行自增操做,那最終的結果應該是 8 。
可是自增的真正實現是分爲下面三步的,而不是一個不可分割的(原子的)操做。
假如線程 1 在進行到第二步的時候,其餘兩條線程讀取了變量 a ,那麼最終的結果就是 7,而不是預期的 8 。
這種現象就是線程安全的其中一個問題:原子性。
現代處理器的處理能力要遠勝於主內存(DRAM)的訪問速率,主內存執行一次內存讀/寫操做須要的時間,若是給處理器使用,處理器能夠執行上百條指令。
爲了彌補處理器與主內存之間的差距,硬件設計者在主內存與處理器之間加入了高速緩存(Cache)。
處理器執行內存讀寫操做時,不是直接與主內存打交道,而是經過高速緩存進行的。
高速緩存至關因而一個由硬件實現的容量極小的散列表,這個散列表的 key 是一個對象的內存地址,value 能夠是內存數據的副本,也能夠是準備寫入內存的數據。
從內部結構來看,高速緩存至關因而一個鏈式散列表(Chained Hash Table),它包含若干個桶,每一個桶包含若干個緩存條目(Cache Entry)。
緩存條目可進一步劃分爲 Tag、Data Block 和 Flag 三個部分。
Tag
Tag 包含了與緩存行中數據對應的內存地址的部分信息(內存地址的高位部分比特)
Data Block
Data Block 也叫緩存行(Cache Line),是高速緩存與主內存之間數據交換的最小單元,能夠存儲從內存中讀取的數據,也能夠存儲準備寫進內存的數據。
Flag
Flag 用於表示對應緩存行的狀態信息
在任意時刻,CPU 只能執行一條機器指令,每一個線程只有獲取到 CPU 的使用權後,才能夠執行指令。
也就是在任意時刻,只有一個線程佔用 CPU,處於運行的狀態。
多線程併發運行其實是指多個線程輪流獲取 CPU 使用權,分別執行各自的任務。
線程的調度由 JVM 負責,線程的調度是按照特定的機制爲多個線程分配 CPU 的使用權。
線程調度模型分爲兩類:分時調度模型和搶佔式調度模型。
分時調度模型
分時調度模型是讓全部線程輪流獲取 CPU 使用權,而且平均分配每一個線程佔用 CPU 的時間片。
搶佔式調度模型
JVM 採用的是搶佔式調度模型,也就是先讓優先級高的線程佔用 CPU,若是線程的優先級都同樣,那就隨機選擇一個線程,並讓該線程佔用 CPU。
也就是若是咱們同時啓動多個線程,並不能保證它們能輪流獲取到均等的時間片。
若是咱們的程序想幹預線程的調度過程,最簡單的辦法就是給每一個線程設定一個優先級。
閱讀完上一節的內容後,咱們對 Java 的線程調度機制有了基本的瞭解。
這一節咱們就來看看線程調度機制致使的線程安全問題,這一節的內容包括如下幾個部分。
線程安全問題不是說線程不安全,也不是說線程弄很差把手機都搞爆炸了。
線程安全問題指的是多個線程之間對一個或多個共享可變對象交錯操做時,有可能致使數據異常。
多線程編程中常常遇到的問題就是同樣的輸入在不一樣的時間有不同的輸出,這種一個計算結果的正確性與時間有關的現象就是競態,也就是計算的正確性依賴於相對時間順序或線程的交錯。
競態不必定致使計算結果的不正確,而是不排除計算結果有時正確有時錯誤的可能。
競態每每伴隨着髒數據和丟失更新的問題,髒數據就是線程讀到一個過期的數據,丟失更新就是一個線程對數據作的更新,沒有體如今後續其餘線程對該數據的讀取上。
對於共享變量,競態能夠當作訪問(讀/寫)同一組共享變量的多個線程鎖執行的操做相互交錯,好比一個線程讀取共享變量,並以該共享變量爲基礎進行計算的期間,另外一個線程更新了該共享變量的值,致使髒數據或丟失更新。
對於局部變量,因爲不一樣的線程各自訪問的是本身的局部變量,因此局部變量的使用不會致使競態。
原子(Atomic)的字面意識是不可分割的,對於涉及共享變量訪問的操做,若該操做從其執行線程之外的任意線程看來是不可分割的,那麼該操做就是原子操做,相應地稱該操做具備原子性(Atomicity)。
所謂不可分割,就是訪問(讀/寫)某個共享變量的操做,從執行線程之外的其餘線程看來,該操做只有未開始和結束兩種狀態,不會知道該操做的中間部分。
拿炒菜舉例,炒菜可分爲幾個步驟:放油、放菜、放鹽、放糖等。
可是從客人的角度來看,一個菜只有兩種狀態:沒作好和作好了。
訪問同一組共享變量的原子操做是不能被交錯的,這就排除了一個線程執行一個操做的期間,另外一個線程讀取或更新該操做鎖訪問的共享變量,致使髒數據和丟失更新。
在多線程環境下,一個線程對某個共享變量進行更新後,後續訪問該變量的線程可能沒法馬上讀取到這個更新的結果,甚至永遠也沒法讀取到這個更新的結果,這就是線程安全問題的另外一種表現形式:可見性。
可見性是指一個線程對共享變量的更新,對於其餘讀取該變量的線程是否可見。
可見性問題與計算機的存儲系統有關,程序中的變量可能會被分配到寄存器而不是主內存中,每一個處理器都有本身的寄存器,一個處理器沒法讀取另外一個處理器的寄存器上的內容。
即便共享變量是分配到主內存中存儲的,也不餓能保證可見性,由於處理器不是直接訪問主內存,而是經過高速緩存進行的。
一個處理器上運行的線程對變量的更新,可能只是更新到該處理器的寫緩衝器(Store Buffer)中,尚未到高速緩存中,更別說處理器了。
可見性描述的是一個線程對共享變量的更新,對於另外一個線程是否可見,保證可見性意味着一個線程能夠讀取到對應共享變量的新值。
從保證線程安全的角度來看,光保證原子性還不夠,還要保證可見性,同時保證可見性和原子性才能確保一個線程能正確地看到其餘線程對共享變量作的更新。
有序性是指一個處理器在爲一個線程執行的內存訪問操做,對於另外一個處理器上運行的線程來看是亂序的。
順序結構是結構化編程中的一種基本結構,它表示咱們但願某個操做先於另一個操做執行。
可是在多核處理器的環境下,代碼的執行順序是沒保障的,編譯器可能改變兩個操做的前後順序,處理器也可能不是按照程序代碼的順序執行指令
重排序(Reordering)處理器和編譯器是對代碼作的一種優化,它能夠在不影響單線程程序正確性的狀況下提高程序的性能,可是它會對多線程程序的正確性產生影響,致使線程安全問題。
現代處理器爲了提升指令的執行效率,每每不是按程序順序注意執行指令的,而是哪條指令就緒就先執行哪條指令,這就是處理器的亂序執行。
要實現線程安全就要保證上面說到的原子性、可見性和有序性。
常見的實現線程安全的辦法是使用鎖和原子類型,而鎖可分爲內部鎖、顯式鎖、讀寫鎖、輕量級鎖(volatile)四種。
下面咱們就來看看這四種鎖和原子類型的用法和特色。
文章的開頭提到的「打架扣 100」就是一種現實生活中的鎖,可讓小張和老王乖乖幹活,別再炒出不能吃的菜。
這也就是鎖(Lock)的做用,讓多個線程更好地協做,避免多個線程的操做交錯致使數據異常的問題。
臨界區
持有鎖的線程得到鎖後和釋放鎖前執行的代碼叫作臨界區(Critical Section)。
排他性
鎖具備排他性,可以保障一個共享變量在任一時刻只能被一個線程訪問,這就保證了臨界區代碼一次只可以被一個線程執行,臨界區的操做具備不可分割性,也就保證了原子性。
串行
鎖至關因而把多個線程對共享變量的操做從併發改成串行。
三種保障
鎖可以保護共享變量實現線程安全,它的做用包括保障原子性、可見性和有序性。
調度策略
鎖的調度策略分爲公平策略和非公平策略,對應的鎖就叫公平鎖和非公平鎖。
公平鎖會在加鎖前查看是否有排隊等待的線程,有的話會優先處理排在前面的線程。
公平鎖以增長上下文切換爲代價,保障了鎖調度的公平性,增長了線程暫停和喚醒的可能性。
鎖泄漏
鎖泄漏是指一個線程得到鎖後,因爲程序的錯誤致使鎖一直沒法被釋放,致使其餘線程一直沒法得到該鎖。
活躍性問題
鎖泄漏會致使活躍性問題,這些問題包括死鎖、和鎖死等。
Java 爲咱們提供了 synchronized 關鍵字來實現內部鎖,被 synchronized 關鍵字修飾的方法和代碼塊就叫同步方法和同步代碼塊。
下面咱們來看下內部鎖的七個特色。
監視器鎖
由於使用 synchronized 實現的線程同步是經過監視器(monitor)來實現的,因此內部鎖也叫監視器鎖。
自動獲取/釋放
線程對同步代碼塊的鎖的申請和釋放由 JVM 內部實施,線程在進入同步代碼塊前會自動獲取鎖,並在退出同步代碼塊時自動釋放鎖,這也是同步代碼塊被稱爲內部鎖的緣由。
鎖定方法/類/對象
synchronized 關鍵字能夠用來修飾方法,鎖住特定類和特定對象。
臨界區
同步代碼塊就是內部鎖的臨界區,線程在執行臨界區代碼前必須持有該臨界區的內部鎖。
鎖句柄
內部鎖鎖的對象就叫鎖句柄,鎖句柄一般會用 private 和 final 關鍵字進行修飾。
由於鎖句柄變量一旦改變,會致使執行同一個同步代碼塊的多個線程實際上用的是不一樣的鎖。
不會泄漏
泄漏指的是鎖泄漏,內部鎖不會致使鎖泄漏,由於 javac 編譯器把同步代碼塊編譯爲字節碼時,對臨界區中可能拋出的異常作了特殊處理,這樣臨界區的代碼出了異常也不會妨礙鎖的釋放。
非公平鎖
內部鎖是使用的是非公平策略,是非公平鎖,也就是不會增長上下文切換開銷。
// 鎖句柄
private final String hello = "hello";
private void getLock1() {
synchronized (hello) {
System.out.println("ThreadA 拿到了內部鎖");
ThreadUtils.sleep(2 * 1000);
}
System.out.println("ThreadA 釋放了內部鎖");
}
複製代碼
private void getLock2() {
System.out.println("ThreadB 嘗試獲取內部鎖");
synchronized (hello) {
System.out.println("ThreadB 拿到了內部鎖");
}
System.out.println("ThreadB 繼續執行");
}
複製代碼
當咱們在兩個線程中分別運行上面兩個函數後,咱們能夠獲得下面的輸出。
ThreadA 拿到了內部鎖
ThreadB 嘗試獲取內部鎖
ThreadA 釋放了內部鎖
ThreadB 拿到了內部鎖
ThreadB 繼續執行
複製代碼
顯式鎖(Explict Lock)是 Lock 接口的實例,Lock 接口對顯式鎖進行了抽象,ReentrantLock 是它的實現類。
下面是顯式鎖的四個特色。
可重入
顯式鎖是可重入鎖,也就是一個線程持有了鎖後,能再次成功申請這個鎖。
手動獲取/釋放
顯式鎖與內部鎖區別在於,使用顯式鎖,咱們要本身釋放和獲取鎖,爲了不鎖泄漏,咱們要在 finally 塊中釋放鎖
臨界區
lock() 與 unlock() 方法之間的代碼就是顯式鎖的臨界區
公平/非公平鎖
顯式鎖容許咱們本身選擇鎖調度策略。
ReentrantLock 有一個構造函數,容許咱們傳入一個 fair 值,當這個值爲 true 時,說明如今建立的這個鎖是一個公平鎖。
因爲公平鎖的開銷比非公平鎖大,因此 ReentrantLock 的默認調度策略是非公平策略。
private final Lock lock = new ReentrantLock();
private void lock1() {
lock.lock();
System.out.println("線程 1 獲取了顯式鎖");
try {
System.out.println("線程 1 開始執行操做");
ThreadUtils.sleep(2 * 1000);
} finally {
lock.unlock();
System.out.println("線程 1 釋放了顯式鎖");
}
}
複製代碼
private void lock2() {
lock.lock();
System.out.println("線程 2 獲取了顯式鎖");
try {
System.out.println("線程 2 開始執行操做");
} finally {
System.out.println("線程 2 釋放了顯式鎖");
lock.unlock();
}
}
複製代碼
當咱們分別在兩個線程中分別執行了上面的兩個函數後,咱們能夠獲得下面的輸出。
線程 1 獲取了顯式鎖
線程 1 開始執行操做
線程 1 釋放了顯式鎖
線程 2 獲取了顯式鎖
線程 2 開始執行操做
線程 2 釋放了顯式鎖
複製代碼
lock()
獲取鎖,獲取失敗時線程會處於阻塞狀態
tryLock()
獲取鎖,獲取成功時返回 true,獲取失敗時會返回 false,不會處於阻塞狀態
tryLock(long time, TimeUnit unit)
獲取鎖,獲取到了會返回 true,若是在指定時間內未獲取到,則返回 false。
在指定時間內處於阻塞狀態,可中斷。
lockInterruptibly()
獲取鎖,可中斷。
看完了內部鎖和顯式鎖的介紹,下面咱們來看下內部鎖和顯式鎖的五個區別。
靈活性
內部鎖是基於代碼的鎖,鎖的申請和釋放只能在一個方法內執行,缺少靈活性。
顯式鎖是基於對象的鎖,鎖的申請和釋放能夠在不一樣的方法中執行,這樣能夠充分發揮面向對象編程的靈活性。
鎖調度策略
內部鎖只能是非公平鎖。
顯式鎖能夠本身選擇鎖調度策略。
便利性
內部鎖簡單易用,不會出現鎖泄漏的狀況。
顯式鎖須要本身手動獲取/釋放鎖,使用不當的話會致使鎖泄漏。
阻塞
若是持有內部鎖鎖的線程一直不釋放這個鎖,那其餘申請這個鎖的線程只能一直等待。
顯式鎖 Lock 接口有一個 tryLock() 方法,當其餘線程持有鎖時,這個方法會返回直接返回 false。
這樣就不會致使線程處於阻塞狀態,咱們就能夠在獲取鎖失敗時作別的事情。
適用場景
在多個線程持有鎖的平均時間不長的狀況下咱們可使用內部鎖
在多個線程持有鎖的平均較長的狀況下咱們可使用顯式鎖(公平鎖)
鎖的排他性使得多個線程沒法以線程安全的方式在同一時刻讀取共享變量,這樣不利於提升系統的併發性,這也是讀寫鎖出現的緣由。
讀寫鎖 ReadWriteLock 接口的實現類是 ReentrantReadWriteLock,。
只讀取共享變量的線程叫讀線程,只更新共享變量的線程叫寫線程。
讀寫鎖是一種改進的排他鎖,也叫共享/排他(Shared/Exclusive)鎖。
讀寫鎖有下面六個特色。
讀鎖共享
讀寫鎖容許多個線程同時讀取共享變量,讀線程訪問共享變量時,必須持有對應的讀鎖,讀鎖能夠被多個線程持有。
寫鎖排他
讀寫鎖一次只容許一個線程更新共享變量,寫線程訪問共享變量時,必須持有對應的寫鎖,寫鎖在任一時刻只能被一個線程持有。
能夠降級
讀寫鎖是一個支持降級的可重入鎖,也就是一個線程在持有寫鎖的狀況下,能夠繼續獲取對應的讀鎖。
這樣咱們能夠在修改變量後,在其餘地方讀取該變量,並執行其餘操做。
不能升級
讀寫鎖不支持升級,讀線程只有釋放了讀鎖才能申請寫鎖
三種保障
讀寫鎖雖然容許多個線程讀取共享變量,可是因爲寫鎖的特性,它一樣能保障原子性、可見性和有序性。
適用場景
讀寫鎖會帶來額外的開銷,只有知足下面兩個條件,讀寫鎖纔是合適的選擇
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
private void write1() {
writeLock.lock();
System.out.println("寫線程1獲取了寫鎖");
try {
System.out.println("寫線程1開始執行操做");
ThreadUtils.sleep(3 * 1000);
} finally {
writeLock.unlock();
System.out.println("寫線程1釋放了寫鎖");
}
}
private void write2() {
writeLock.lock();
System.out.println("寫線程2獲取了寫鎖");
try {
System.out.println("寫線程2開始執行操做");
} finally {
writeLock.unlock();
System.out.println("寫線程2釋放了寫鎖");
}
}
複製代碼
private void read1() {
readLock.lock();
System.out.println("讀線程1獲取了讀鎖");
try {
System.out.println("讀線程1開始執行操做");
ThreadUtils.sleep(3 * 1000);
} finally {
readLock.unlock();
System.out.println("讀線程1釋放了讀鎖");
}
}
private void read2() {
readLock.lock();
System.out.println("讀線程2獲取了讀鎖");
try {
System.out.println("讀線程2開始執行操做");
ThreadUtils.sleep(3 * 1000);
} finally {
readLock.unlock();
System.out.println("讀線程2釋放了讀鎖");
}
}
複製代碼
當在四個線程中分別執行上面的四個函數時,咱們能夠獲得下面的輸出。
寫線程1獲取了寫鎖
寫線程1開始執行操做
寫線程1釋放了寫鎖
寫線程2獲取了寫鎖
寫線程2開始執行操做
寫線程2釋放了寫鎖
讀線程1獲取了讀鎖
讀線程1開始執行操做
讀線程2獲取了讀鎖
讀線程2開始執行操做
讀線程1釋放了讀鎖
讀線程2釋放了讀鎖
複製代碼
volatile 關鍵字可用於修飾共享變量,對應的變量就叫 volatile 變量,volatile 變量有下面幾個特色。
易變化
volatile 的字面意思是「不穩定的」,也就是 volatile 用於修飾容易發生變化的變量,不穩定指的是對這種變量的讀寫操做要從高速緩存或主內存中讀取,而不會分配到寄存器中。
開銷
比鎖低
volatile 的開銷比鎖低,volatile 變量的讀寫操做不會致使上下文切換,因此 volatile 關鍵字也叫輕量級鎖 。
比普通變量高
volatile 變量讀操做的開銷比普通變量要高,這是由於 volatile 變量的值每次都要從高速緩存或主內存中讀取,沒法被暫存到寄存器中。
釋放/存儲屏障
對於 volatile 變量的寫操做,JVM 會在該操做前插入一個釋放屏障,並在該操做後插入一個存儲屏障。
存儲屏障具備沖刷處理器緩存的做用,因此在 volatile 變量寫操做後插入一個存儲屏障,能讓該存儲屏障前的全部操做結果對其餘處理器來講是同步的。
加載/獲取屏障
對於 volatile 變量的讀操做,JVM 會在該操做前插入一個加載屏障,並在操做後插入一個獲取屏障。
加載屏障經過沖刷處理器緩存,使線程所在的處理器將其餘處理器對該共享變量作的更新同步到該處理器的高速緩存中。
保證有序性
volatile 能禁止指令重排序,也就是使用 volatile 能保證操做的有序性。
保證可見性
讀線程執行的加載屏障和寫線程執行的存儲屏障配合在一塊兒,能讓寫線程對 volatile 變量的寫操做對讀線程可見,從而保證了可見性。
原子性
在原子性方面,對於 long/double 型變量,volatile 能保證讀寫操做的原子型。
對於非 long/double 型變量,volatile 只能保證寫操做的原子性。
若是 volatile 變量寫操做前涉及共享變量,競態仍然可能發生,由於共享變量賦值給 volatile 變量時,其餘線程可能已經更新了該共享變量的值。
在 JUC 下有一個 atomic 包,這個包裏面有一組原子類,使用原子類的方法,不須要加鎖也能保證線程安全,而原子類是經過 Unsafe 類中的 CAS 指令從硬件層面來實現線程安全的。
這個包裏面有如 AtomicInteger、AtomicBoolean、AtomicReference、AtomicReferenceFIeldUpdater 等。
咱們先來看一個使用原子整型 AtomicInteger 自增的例子。
// 初始值爲 1
AtomicInteger integer = new AtomicInteger(1);
// 自增
int result = integer.incrementAndGet();
// 結果爲 2
System.out.println(result);
複製代碼
AtomicReference 和 AtomicReferenceFIeldUpdater 可讓咱們本身的類具備原子性,它們的原理都是經過 Unsafe 的 CAS 操做實現的。
咱們下面看下它們的用法和區別。
class AtomicReferenceValueHolder {
AtomicReference<String> atomicValue = new AtomicReference<>("HelloAtomic");
}
public void getAndUpdateFromReference() {
AtomicReferenceValueHolder holder = new AtomicReferenceValueHolder();
// 對比並設值
// 若是值是 HelloAtomic,就把值換成 World
holder.atomicValue.compareAndSet("HelloAtomic", "World");
// World
System.out.println(holder.atomicValue.get());
// 修改並獲取修改後的值
String value = holder.atomicValue.updateAndGet(new UnaryOperator<String>() {
@Override
public String apply(String s) {
return "HelloWorld";
}
});
// Hello World
System.out.println(value);
}
複製代碼
AtomicReferenceFieldUpdater 在用法上和 AtomicReference 有些不一樣,咱們直接把 String 值暴露了出來,而且用 volatile 對這個值進行了修飾。
而且將當前類和值的類傳到 newUpdater ()方法中獲取 Updater,這種用法有點像反射,並且 AtomicReferenceFieldUpdater 一般是做爲類的靜態成員使用。
public class SimpleValueHolder {
public static AtomicReferenceFieldUpdater<SimpleValueHolder, String> valueUpdater
= AtomicReferenceFieldUpdater.newUpdater(
SimpleValueHolder.class, String.class, "value");
volatile String value = "HelloAtomic";
}
public void getAndUpdateFromUpdater() {
SimpleValueHolder holder = new SimpleValueHolder();
holder.valueUpdater.compareAndSet(holder, "HelloAtomic", "World");
// World
System.out.println(holder.valueUpdater.get(holder));
String value = holder.valueUpdater.updateAndGet(holder, new UnaryOperator<String>() {
@Override
public String apply(String s) {
return "HelloWorld";
}
});
// HelloWorld
System.out.println(value);
}
複製代碼
AtomicReference 和 AtomicReferenceFieldUpdater 的做用是差很少的,在用法上 AtomicReference 比 AtomicReferenceFIeldUpdater 更簡單。
可是在內部實現上,AtomicReference 內部同樣是有一個 volatile 變量。
使用 AtomicReference 和使用 AtomicReferenceFIeldUpdater 比起來,要多建立一個對象。
對於 32 位的機器,這個對象的頭佔 12 個字節,它的成員佔 4 個字節,也就是多出來 16 個字節。
對於 64 位的機器,若是啓動了指針壓縮,那這個對象佔用的也是 16 個字節。
對於 64 位的機器,若是沒啓動指針壓縮,那麼這個對象就會佔 24 個字節,其中對象頭佔 16 個字節,成員佔 8 個字節。
當要使用 AtomicReference 建立成千上萬個對象時,這個開銷就會變得很大。
這也就是爲何 BufferedInputStream 、Kotlin 協程 和 Kotlin 的 lazy 的實現會選擇 AtomicReferenceFieldUpdater 做爲原子類型。
由於開銷的緣由,因此通常只有在原子類型建立的實例肯定了較少的狀況下,好比說是單例,纔會選擇 AtomicReference,不然都是用 AtomicReferenceFieldUpdater。
使用鎖會帶來必定的開銷,而掌握鎖的使用技巧能夠在必定程度上減小鎖帶來的開銷和潛在的問題,下面就是一些鎖的使用技巧。
長鎖不如短鎖
儘可能只對必要的部分加鎖
大鎖不如小鎖
進可能對加鎖的對象拆分
公鎖不如私鎖
進可能把鎖的邏輯放到私有代碼中,若是讓外部調用者加鎖,可能會致使鎖不正當使用致使死鎖
嵌套鎖不如扁平鎖
在寫代碼時要避免鎖嵌套
分離讀寫鎖
儘量將讀鎖和寫鎖分離
粗化高頻鎖
合併處理頻繁並且太短的鎖,由於每一把鎖都會帶來必定的開銷
消除無用鎖
儘量不加鎖,或者用 volatile 代替
上一大節介紹了鎖的做用和基本用法,鎖能讓線程進入阻塞狀態,而這種阻塞就會致使任務沒法正常執行,也就是線程出現活躍性問題,這也就是咱們這一節要講的內容。
活躍性問題不是說線程過於活躍,而是線程不夠活躍,致使任務沒法取得進展。
咱們這一節就來看一下常見的四個線程活躍性問題:死鎖、鎖死、活鎖和飢餓。
死鎖是線程的一種常見多線程活躍性問題,若是兩個或更多的線程,由於相互等待對方而被永遠暫停,那麼這就叫死鎖現象。
下面咱們就來看看死鎖產生的四個條件和避免死鎖的三個方法。
當多個線程發生了死鎖後,這些線程和相關共享變量就會知足下面四個條件。
資源互斥
涉及的資源必須是獨佔的,也就是資源每次只能被一個線程使用
資源不可搶奪
涉及的資源只能被持有該資源的線程主動釋放,沒法被其餘線程搶奪(被動釋放)
佔用並等待資源
涉及的線程至少持有一個資源,還申請了其餘資源,而其餘資源恰好被其餘線程持有,而且線程不釋放已持有資源
循環等待資源
涉及的線程必須等待別的線程持有的資源,而別的線程又反過來等待該線程持有的資源
只要產生了死鎖,上面的條件就必定成立,可是上面的條件都成立也不必定會產生死鎖。
要想消除死鎖,只要破壞掉上面的其中一個條件便可。
因爲鎖具備排他性,且沒法被動釋放,因此咱們只能破壞掉第三個和第四個條件。
粗鎖法
使用粗粒度的鎖代替多個鎖,鎖的範圍變大了,訪問共享資源的多個線程都只須要申請一個鎖,由於每一個線程只須要申請一個鎖就能夠執行本身的任務,這樣「佔用並等待資源」和「循環等待資源」這兩個條件就不成立了。
粗鎖法的缺點是會下降併發性,並且可能致使資源浪費,由於採用粗鎖法時,一次只能有一個線程訪問資源,這樣其餘線程就只能擱置任務了。
鎖排序法
鎖排序法指的是相關線程使用全局統一的順序申請鎖。
假若有多個線程須要申請鎖,咱們只須要讓這些線程按照一個全局統一的順序去申請鎖,這樣就能破壞「循環等待資源」這個條件。
tryLock
顯式鎖 ReentrantLock.tryLock(long timeUnit) 這個方法容許咱們爲申請鎖的操做設置超時時間,這樣就能破壞「佔用並等待資源」這個條件。
開放調用
開放調用(Open Call)就是一個方法在調用外部方法時不持有鎖,開放調用能破壞「佔用並等待資源」這個條件。
等待線程因爲喚醒的條件永遠沒法成立,致使任務一直沒法繼續執行,那麼這個線程是被鎖死(Lockout)了。
鎖死和死鎖的區別在於,即便產生死鎖的條件所有都不成立,仍是有可能發生鎖死。
鎖死可分爲信號丟失鎖死和嵌套監視器鎖死。
信號丟失鎖死是因爲沒有對應的通知線程喚醒等待線程,致使等待線程一直處於等待狀態的一種活躍性問題。
信號丟失鎖死的一個典型例子就是等待線程執行 Object.wait()/Condition.await() 前沒有判斷保護條件,而保護條件已經成立,可是後續沒有其餘線程更新保護條件並通知等待線程,這也就是爲何要強調 Object.wait()/Condition.await() 要放在循環語句中執行。
嵌套監視器鎖死指的是嵌套地使用鎖致使線程永遠沒法被喚醒,在代碼上的表現就是兩個嵌套的同步代碼塊。
避免嵌套監視器鎖死的辦法只須要避免嵌套使用內部鎖。
活鎖(Livelock)是指線程一直處於運行狀態,可是任務卻一直沒法繼續執行的一種現象。
線程飢餓(Starvation)是指線程一直沒法得到所需資源,致使任務一直沒法執行。
線程間的常見協做方式有兩種:等待和中斷。
中斷型協做放在第 8 大節講,咱們這一節主要講等待型協做。
當一個線程中的操做須要等待另外一個線程中的操做結束時,就涉及到等待型線程協做方式。
經常使用的等待型線程協做方式有 join、wait/notify、await/signal、await/countDown 和 CyclicBarrier 五種,下面咱們就來看看這五種線程協做方式的用法和區別。
使用 Thread.join() 方法,咱們可讓一個線程等待另外一個線程執行結束後再繼續執行。
join() 方法實現等待是經過 wait() 方法實現的,在 join() 方法中,會不斷判斷調用了 join() 方法的線程是否還存活,是的話則繼續等待。
下面是 join() 方法的簡單用法。
public void tryJoin() {
Thread threadA = new ThreadA();
Thread threadB = new ThreadB(threadA);
threadA.start();
threadB.start();
}
複製代碼
public class ThreadA extends Thread {
@Override
public void run() {
System.out.println("線程 A 開始執行");
ThreadUtils.sleep(1000);
System.out.println("線程 A 執行結束");
}
}
複製代碼
public class ThreadB extends Thread {
private final Thread threadA;
public ThreadB(Thread thread) {
threadA = thread;
}
@Override
public void run() {
try {
System.out.println("線程 B 開始等待線程 A 執行結束");
threadA.join();
System.out.println("線程 B 結束等待,開始作本身想作的事情");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製代碼
當咱們執行完上面的代碼後,會獲得下面的輸出。
線程 A 開始執行
線程 B 開始等待線程 A 執行結束
線程 A 執行結束
線程 B 結束等待,開始作本身想作的事情
複製代碼
在 Java 中,使用 Object.wait()/Object.wait(long) 和 Object.notify()/Object.notifyAll() 能夠用於實現等待和通知。
一個線程由於執行操做(目標動做)所需的保護條件未知足而被暫停的過程就叫等待(wait)。
一個線程更新了共享變量,使得其餘線程須要的保護條件成立,喚醒了被暫停的線程的過程就叫通知(notify)。
wait() 方法的執行線程叫等待線程,notify() 方法執行的線程叫通知線程。
wait/notify 協做方式有下面幾個特色。
暫停/喚醒
Object.wait() 的做用是讓線程暫停(狀態改成 WAITING),而 Object.notify() 的做用是喚醒一個被暫停的線程。
全部對象
因爲 Object 是全部對象的父類,因此全部對象均可以實現等待和通知。
獲取監視器鎖
使用 wait()/notify() 方法要先獲取共享對象的監視器鎖,獲取共享對象的監視器鎖有兩種方式,一是在同步代碼塊中執行,二是在同步方法(synchronized 修飾的方法)中執行 wait()/notify()。
若是沒有事先獲取監視器鎖,那線程就會報出非法監視器狀態異常 IllegalMonitorStateException 異常。
捕獲中斷異常
使用 wait() 方法必需要捕獲中斷異常 InterruptedException,由於經過 wait() 進入的等待狀態是能夠被打斷的。
喚醒任一線程
notify() 方法喚醒的只是對應對象上的一個任意等待線程,被喚醒的線程不必定是咱們想喚醒的線程。
喚醒特定線程
若是咱們想對應對象上的特定線程,咱們可使用 notifyAll(),把該對象上的全部等待線程都喚醒。
final 修飾
之因此 lock 對象要使用 final 修飾,是由於若是沒有用 final 修飾,那麼這個對象的值可能被修改,致使等待線程和通知線程同步在不一樣的內部鎖上,從而形成競態,違背了使用鎖的初衷。
循環判斷
對保護條件的判斷和 wait() 方法的調用要放在循環語句中,以確保目標動做只有在保護條件成立時才能執行。
僅釋放對應內部鎖
使用 wait() 方法暫停當前線程時,釋放的鎖是與該 wait() 方法所屬對象的內部鎖,當前線程持有的其餘內部鎖和顯式鎖不會所以被釋放
下面是 wait/notify 使用的示例代碼。
final Object lock = new Object();
private volatile boolean conditionSatisfied;
public void startWait() throws InterruptedException {
synchronized (lock) {
System.out.println("等待線程獲取了鎖");
while(!conditionSatisfied) {
System.out.println("保護條件不成立,等待線程進入等待狀態");
lock.wait();
}
System.out.println("等待線程被喚醒,開始執行目標動做");
}
}
複製代碼
public void startNotify() {
synchronized (lock) {
System.out.println("通知線程獲取了鎖");
System.out.println("通知線程即將喚醒等待線程");
conditionSatisfied = true;
lock.notify();
}
}
複製代碼
當咱們在兩個線程中分別執行上面兩個函數後,會獲得下面的輸出。
等待線程獲取了鎖
保護條件不成立,等待線程進入等待狀態
通知線程獲取了鎖
通知線程即將喚醒等待線程
等待線程被喚醒,開始執行目標動做
複製代碼
JVM 會給每一個對象維護一個入口集(Entry Set)和等待集(Wait Set)。
入口集用於存儲申請該對象內部鎖的線程,等待集用於存儲對象上的等待線程。
wait() 方法會將當前線程暫停,在釋放內部鎖時,會將當前線程存入該方法所屬的對象等待集中。
調用對象的 notify() 方法,會讓該對象的等待集中的任意一個線程喚醒,被喚醒的線程會繼續留在對象的等待集中,直到該線程再次持有對應的內部鎖時,wait() 方法就會把當前線程從對象的等待集中移除。
添加當前線程到等待集、暫停當前線程、釋放鎖以及把喚醒後的線程從對象的等待集中移除,都是在 wait() 方法中實現的。
在 wait() 方法的 native 代碼中,會判斷線程是否持有當前對象的內部鎖,若是沒有的話,就會報非法監視器狀態異常,這也就是爲何要在同步代碼塊中執行 wait() 方法。
過早喚醒
等待線程在保護條件未成立時被喚醒的現象就叫過早喚醒。
過早喚醒使得無須被喚醒的等待線程也被喚醒了,致使資源浪費。
信號丟失
致使信號丟失的狀況有兩種,一種是在循環體外判斷保護條件,另外一種是 notify() 方法使用不當。
循環體外判斷條件
若是等待線程在執行 wait() 方法前沒有判斷保護條件是否成立,那麼有可能致使通知線程在等待線程進入臨界區前就更新了共享變量,使得保護條件成立,並進行了通知,可是等待線程並無暫停,因此也沒有被喚醒。
這種現象至關於等待線程錯過了一個發送給它的「信號」,因此叫信號丟失。
只要對保護條件的判斷和 wait() 方法的調用放在循環語句中,就能夠避免這種狀況致使的信號丟失。
notify() 使用不當
信號丟失的另外一個表現是在應該調用 notifyAll() 的狀況下調用了 notify(),在這種狀況下,避免信號丟失的辦法是使用 notifyAll() 進行通知
欺騙性喚醒
等待線程可能在沒有其餘線程執行 notify()/notifyAll() 的狀況下被喚醒,這種現象叫欺騙性喚醒。
雖然欺騙性喚醒出現的機率比較低,可是 Java 容許這種現象存在,這是 Java 平臺對操做系統妥協的一種結果。
避免欺騙性喚醒
避免欺騙性喚醒的方法就是在循環中判斷條件是否知足,不知足時則繼續等待,也就是再次調用 wait() 方法。
上下文切換
等待線程執行 wait() 方法至少會致使該線程對內部鎖的兩次申請與釋放。
通知線程在執行 notify()/notifyAll() 時須要持有對應對象的內部鎖,因此這裏會致使一次鎖的申請,而鎖的申請與釋放可能致使上下文切換。
其次,等待線程從被暫停到喚醒的過程自己就會致使上下文切換。
再次,被喚醒的等待線程在繼續運行時,須要再次申請內部鎖,此時等待線程可能須要和對應對象的入口集中的其餘線程,以及其餘新來的活躍線程爭用內部鎖,這又可能致使上下文切換。
最後,過早喚醒也會致使額外的上下文切換,由於被過早喚醒的線程須要繼續等待,要再次經歷被暫停和喚醒的過程。
減小 wait/notify 上下文切換的經常使用方法有下面兩種。
使用 notify() 代替 notifyAll()
在保證程序正確性的狀況下,使用 notify() 代替 notifyAll(),notify() 不會致使過早喚醒,從而減小上下文切換開銷
儘快釋放對應內部鎖
通知線程執行完 notify()/notifyAll() 後儘快釋放對應的內部鎖,這樣能夠避免被喚醒的線程在 wait() 調用返回前,再次申請對應內部鎖時,因爲該鎖未被通知線程釋放,致使該線程被暫停
notify() 可能致使信號丟失,而 notifyAll() 雖然會把不須要喚醒的等待線程也喚醒,可是在正確性方面有保障。
因此通常狀況下優先使用 notifyAll() 保障正確性。
通常狀況下,只有在下面兩個條件都實現時,纔會選擇使用 notify() 實現通知。
只需喚醒一個線程
當一次通知只須要喚醒最多一個線程時,咱們能夠考慮使用 notify() 實現通知,可是光知足這個條件還不夠。
在不一樣的等待線程使用不一樣的保護條件時,notify() 喚醒的一個任意線程可能不是咱們須要喚醒的那個線程,因此須要條件 2 來排除。
對象的等待集中只包含同質等待線程
同質等待線程指的是線程使用同一個保護條件而且 wait() 調用返回後的邏輯一致。
最典型的同質線程是使用同一個 Runnable 建立的不一樣線程,或者同一個 Thread 子類 new 出來的多個實例。
wait()/notify() 過於底層,並且還存在兩個問題,一是過早喚醒、二是沒法區分 Object.wait(ms) 返回是因爲等待超時仍是被通知線程喚醒。
使用 await/signal 協做方式有下面幾個要點。
Condition 接口
在 JDK 5 中引入了 Condition(條件變量) 接口,使用 Condition 也能夠實現等待/通知,並且不存在上面提到的兩個問題。
Condition 接口提供的 await()/signal()/signalAll() 至關因而 Object 提供的 wait()/notify()/notifyAll()。
經過 Lock.newCondition() 能夠得到一個 Condition 實例。
持有鎖
與 wait/notify 相似,wait/notify 須要線程持有所屬對象的內部鎖,而 await/signal 要求線程持有 Condition 實例的顯式鎖。
等待隊列
Condition 實例也叫條件變量或條件隊列,每一個 Condition 實例內部都維護了一個用於存儲等待線程的等待隊列,至關因而 Object 中的等待集。
循環語句
對於保護條件的判斷和 await() 方法的調用,要放在循環語句中
引導區內
循環語句和執行目標動做要放在同一個顯式鎖引導的臨界區中,這麼作是爲了不欺騙性喚醒和信號丟失的問題
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private volatile boolean conditionSatisfied = false;
private void startWait() {
lock.lock();
System.out.println("等待線程獲取了鎖");
try {
while (!conditionSatisfied) {
System.out.println("保護條件不成立,等待線程進入等待狀態");
condition.await();
}
System.out.println("等待線程被喚醒,開始執行目標動做");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("等待線程釋放了鎖");
}
}
複製代碼
public void startNotify() {
lock.lock();
System.out.println("通知線程獲取了鎖");
try {
conditionSatisfied = true;
System.out.println("通知線程即將喚醒等待線程");
condition.signal();
} finally {
System.out.println("通知線程釋放了鎖");
lock.unlock();
}
}
複製代碼
當咱們在兩個線程中分別執行了上面的兩個函數後,能獲得下面的輸出。
等待線程獲取了鎖
保護條件不成立,等待線程進入等待狀態
通知線程獲取了鎖
通知線程即將喚醒等待線程
等待線程被喚醒,開始執行目標動做
複製代碼
上面咱們說到 Condition 接口能夠解決 Object.wait(ms) 沒法判斷等待的結束是因爲超時仍是喚醒,而解決辦法就是使用 awaitUntil(timeout, unit) 方法。
若是是因爲超時致使等待結束,那麼 awaitUntil() 會返回 false,不然會返回 true,表示等待是被喚醒的,下面咱們就看看這個方法是怎麼用的。
private void startTimedWait() throws InterruptedException {
lock.lock();
System.out.println("等待線程獲取了鎖");
// 3 秒後超時
Date date = new Date(System.currentTimeMillis() + 3 * 1000);
boolean isWakenUp = true;
try {
while (!conditionSatisfied) {
if (!isWakenUp) {
System.out.println("已超時,結束等待任務");
return;
} else {
System.out.println("保護條件不知足,而且等待時間未到,等待進入等待狀態");
isWakenUp = condition.awaitUntil(date);
}
}
System.out.println("等待線程被喚醒,開始執行目標動做");
} finally {
lock.unlock();
}
}
複製代碼
public void startDelayedNotify() {
threadSleep(4 * 1000);
startNotify();
}
複製代碼
等待線程獲取了鎖
保護條件不知足,而且等待時間未到,等待進入等待狀態
已超時,結束等待任務
通知線程獲取了鎖
通知線程即將喚醒等待線程
複製代碼
使用 join() 實現的是一個線程等待另外一個線程執行結束,可是有的時候咱們只是想要一個特定的操做執行結束,不須要等待整個線程執行結束,這時候就可使用 CountDownLatch 來實現。
await/countDown 協做方式有下面幾個特色。
先決操做
CountDownLatch 能夠實現一個或多個線程等待其餘線程完成一組特定的操做後才繼續運行,這組線程就叫先決操做。
先決操做數
CountDownLatch 內部維護了一個用於計算未完成先決操做數的 count 值,每當 CountDownLatch.countDown() 方法執行一次,這個值就會減 1。
未完成先決操做數 count 是在 CountDownLatch 的構造函數中設置的。
要注意的是,這個值不能小於 0,不然會報非法參數異常。
一次性
當計數器的值爲 0 時,後續再調用 await() 方法不會再讓執行線程進入等待狀態,因此說 CountDownLatch 是一次性協做。
不用加鎖
CountDownLatch 內部封裝了對 count 值的等待和通知邏輯,因此在使用 CountDownLatch 實現等待/通知不須要加鎖
await()
CountDownLatch.await() 可讓線程進入等待狀態,當 CountDownLatch 中的 count 值爲 0 時,表示須要等待的先決操做已經完成。
countDown()
調用 CountDownLatch.countDown() 方法後,count 值就會減 1,而且在 count 值爲 0 時,會喚醒對應的等待線程。
public void tryAwaitCountDown() {
startWaitThread();
startCountDownThread();
startCountDownThread();
}
複製代碼
final int prerequisiteOperationCount = 2;
final CountDownLatch latch = new CountDownLatch(prerequisiteOperationCount);
private void startWait() throws InterruptedException {
System.out.println("等待線程進入等待狀態");
latch.await();
System.out.println("等待線程結束等待");
}
複製代碼
private void startCountDown() {
try {
System.out.println("執行先決操做");
} finally {
System.out.println("計數值減 1");
latch.countDown();
}
}
複製代碼
當咱們在兩個線程中分別執行 startWait() 和 startCountDown() 方法後,咱們會獲得下面的輸出。
等待線程進入等待狀態
執行先決操做
計數值減 1
執行先決操做
計數值減 1
等待線程結束等待
複製代碼
有的時候多個線程須要互相等待對方代碼中的某個地方(集合點),這些線程才能繼續執行,這時可使用 CyclicBarrier(柵欄)。
CyclicBarrier 是 JDK 5 引入的一個類,CyclicBarrier 協做方式有下面幾個特色。
使用 CyclicBarrier.await() 實現等待的線程叫參與方(Party),除了最後一個執行 CyclicBarrier.await() 方法的線程外,其餘執行該方法的線程都會被暫停。
和 CountDownLatch 不一樣,CyclicBarrier 是能夠重複使用的,也就是等待結束後,能夠再次進行一輪等待。
老王和小張成天這麼整也不是辦法,有一天老李就想了個辦法,組織幾天登山,下面咱們就來看看在登山前他們都作了什麼。
final int parties = 3;
final Runnable barrierAction = new Runnable() {
@Override
public void run() {
System.out.println("人來齊了,開始登山");
}
};
final CyclicBarrier barrier = new CyclicBarrier(parties, barrierAction);
public void tryCyclicBarrier() {
firstDayClimb();
secondDayClimb();
}
private void firstDayClimb() {
new PartyThread("第一天登山,老李先來").start();
new PartyThread("老王到了,小張還沒到").start();
new PartyThread("小張到了").start();
}
private void secondDayClimb() {
new PartyThread("次日登山,老王先來").start();
new PartyThread("小張到了,老李還沒到").start();
new PartyThread("老李到了").start();
}
複製代碼
public class PartyThread extends Thread {
private final String content;
public PartyThread(String content) {
this.content = content;
}
@Override
public void run() {
System.out.println(content);
try {
barrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製代碼
運行上面的代碼後,能夠獲得下面的輸出。
第一天登山,老李先來
老王到了,小張還沒到
小張到了
人來齊了,開始登山
次日登山,老王先到
小張到了,老李還沒到
老李到了
人來齊了,開始登山
複製代碼
CyclicBarrier 內部有一個用於實現等待/通知的 Condition(條件變量)類型的變量 trip 。
並且 CyclicBarrier 內部還有一個分代(Generation)對象,用於表示CyclicBarrier 實例是能夠重複使用的。
當前分代的初始狀態是 parties(參與方總數),CyclicBarrier.await() 方法每執行一次,parties 的值就會減 1。
調用了 CyclicBarrier 方法的參與方至關因而等待線程,而最後一個參與方至關因而通知線程。
當最後一個參與方調用了 CyclicBarrier.await() 方法時,在該方法中會先執行 barrierAction.run() ,再執行 trip.signalAll() 喚醒全部等待線程,接着開始下一個分代,也就是 parties 的值會恢復爲初始值。
Generation 中有一個布爾值 broken,當調用 CyclicBarrier.await() 方法的線程被中斷時,broken 的值就會變爲 true。
這時會拋出一個 BrokenBarrierExcetpion 異常,這個異經常使用於表示當前分代已經被破壞了,沒法完成該分代應該完成的任務了。
也就是使用 CyclicBarrier 的每個線程,都不能被中斷(interrupt() 方法被調用)。
JDK 中的 stop() 方法很早就被棄用了,之因此會被棄用,咱們能夠來看下 stop() 方法可能致使的兩種狀況。
第一種狀況,假如如今有線程 A 和 線程 B,線程 A 持有了線程 B 須要的鎖,而後線程 A 被 stop() 強行結束了,致使這個鎖沒有被釋放,那線程 B 就一直拿不到這個鎖了,至關因而線程 B 中的任務永遠沒法執行了。
第二種狀況,假如線程 A 正在修改一個變量,修改到一半,而後被 stop() 強行結束了,這時候線程 B 去讀取這個變量,讀取到的就是一個異常值,這就可能致使線程 B 出現異常。
由於上述兩種資源清理的問題,因此如今不少語言都廢棄了線程的 stop() 方法。
雖然線程不能被簡單粗暴地終止,可是線程執行的任務是能夠中止的,下面咱們就來看看怎麼中止任務。
當咱們調用 sleep() 方法時,編譯器會要求咱們捕獲中斷異常 InterruptedException,這是由於線程的休眠狀態可能會被中斷。
在線程休眠期間,若是其餘地方調用了線程的 interrupt() 方法,那麼這個休眠狀態就會被中斷,中斷後就會接收到一箇中斷異常。
咱們能夠在捕獲到中斷異常後釋放鎖,好比關閉流或文件。
可是調用線程的 interrupt() 方法不是百分百能中斷任務的,假如咱們如今有一個線程,它的 run() 方法中有個 while 循環在執行某些操做,那麼在其餘地方調用該線程的 interrupt() 方法並不能中斷這個任務。
在這種狀況下,咱們能夠經過 interrupted() 或 isInterruped() 方法判斷任務是否被中斷。
interrupted() 與 isInterrupted() 方法均可以獲取線程的中斷狀態,但它們有下面一些區別。
靜態
interrupted() 是靜態方法,isInterrupted() 是非靜態方法
重置
interrupted() 會重置中斷狀態,也就是無論此次獲取到的中斷狀態是 true 仍是 false,下次獲取到的中斷狀態都是 false
isInterrupted() 不會重置中斷狀態,也就是調用了線程的 interrupt() 方法後,經過該方法獲取到的中斷狀態會一直爲 true
不管是使用 interrupted() 仍是 isInterrupted() 方法,本質上都是經過 Native 層的布爾標誌位判斷的。
既然 interrupt() 只是對布爾值的一個修改,那咱們能夠在 Java 層本身設一個布爾標誌位,讓每一個線程共享這個布爾值。
當咱們想取消某個任務時,就在外部把這個標誌位改成 true。
注意事項
直接使用布爾標誌位會有可見性問題,因此要用 volatile 關鍵字修飾這個值。
使用場景
當咱們須要用到 sleep() 方法時,咱們可使用 interrupt() 來中斷任務,其餘時候可使用布爾標誌位。
ConcurrentHashMap 是一個併發容器,併發容器是相對於同步容器的一個概念。
咱們常用的 HashMap 和 ArrayList 等數據容器是線程不安全的,好比使用 HashMap 時須要本身加鎖,這時候就須要線程安全的數據容器:同步容器和異步容器。
同步容器指的是 Hashtable 等線程安全的數據容器,同步容器實現線程安全的方式存在性能問題。
同步容器之一的 Hashtable 存在以下的問題。
大鎖
對 Hashtable 對象加鎖
長鎖
直接對方法加鎖
讀寫鎖共用
只有一把鎖,從頭鎖到尾
而併發容器好比 ConcurrentHashMap、CopyOnWriteArrayList 等就不存在這個問題,下面我就來看看它們是怎麼實現的。
ConcurrentHashMap 從 JDK 5~8 ,每個版本都進行了優化,下面咱們就看下各個版本對 ConcurrentHashMap 作的優化。
JDK 5
在 JDK 5 中,ConcurrentHashMap 的實現是使用分段鎖,在必要時加鎖。
Hashtable 是整個哈希表加鎖,而 JDK 5 引入的 ConcurrentHashMap 使用段(Segment)存儲鍵值對,在必要時對段進行加鎖,不一樣段之間的訪問不受影響。
JDK 5 的 ConcurrentHashMap 中的哈希算法對於比較小的整數,好比三萬如下的整數做爲 key 時,沒法讓元素均勻分佈在各個段中,致使它退化成了一個 Hashtable。
JDK 6
在 JDK 6 中,ConcurrentHashMap 優化了二次 Hash 算法,用了 single-word Wang/Jenkins 哈希算法,這個算法可讓元素均勻分佈在各個段中。
JDK 7
JDK 7 的 ConcurrentHashMap 初始化段的方式跟以前的版本不同,之前是 ConcurrentHashMap 構造出來後直接實例化 16 個段,而 JDK 7 開始,是須要哪一個就建立哪一個。
懶加載實例化段會涉及可見性問題,因此在 JDK 7 的 ConcurrentHashMap 中使用了 volatile 和 UNSAFE.getObjectVolatile() 來保證可見性。
JDK 8
在 JDK 8 中,ConcurrentHashMap 廢棄了段這個概念,實現改成基於 HashMap 原理進行併發化。
對沒必要加鎖的地方,儘可能使用 volatile 進行訪問,對於必定要加鎖的操做,會選擇小的範圍加鎖。
小鎖
分段鎖(JDK 5~7)
桶節點鎖(JDK 8)
短鎖
先嚐試獲取,失敗再加鎖
分離讀寫鎖
讀失敗再加鎖(JDK 5~7)
volatile 讀 CAS 寫(JDK 7~8)
弱一致性
在使用線程執行異步任務的過程當中,咱們要準收一些使用準則,這樣能在必定程度上避免使用線程的時候帶來的問題。
常見的五個線程使用準則是:嚴謹直接建立線程、使用基礎線程池、選擇合適的異步方式、線程必須命名以及重視優先級設置。
嚴禁直接建立線程
直接建立線程除了簡單方便以外,沒有其餘優點,因此在實際項目開發過程當中,必定要嚴禁直接建立線程執行異步任務。
提供基礎線程池供各個業務線使用
這個準則是爲了不各個業務線各自維護一套線程池,致使線程數過多。
假如咱們有 10 條業務線,若是每條業務線都維護一個線程池,假如這個線程池的核心數是 8,那麼咱們就有 80 條線程,這明顯是不合理的。
選擇合適的異步方式
HandlerThread、IntentService 和 RxJava 等方式均可以執行異步任務,可是要根據任務類型來選擇合適的異步方式。
假如咱們有一個可能會長時間執行,可是優先級較低的任務,咱們就能夠選擇用 HandlerThread。
還有一種狀況就是咱們須要執行一個定時任務,這種狀況下更適合使用線程池來操做。
線程必須命名
當咱們開發組成員比較多的時候,不管是使用線程仍是使用線程池,若是咱們不對咱們建立的線程命名,若是這個線程發生了異常,咱們光靠默認線程名是不知道要找哪一個開發人員的。
若是咱們對每一個線程都命名了,就能夠快速地定位到線程的建立者,能夠把問題交給他來解決。
咱們能夠在運行期經過 Thread.currentThread().setName(name) 修改線程的名字。
若是在一段時間內是咱們業務線使用,咱們能夠把線程的名字改爲咱們業務線的標誌,在任務完成後,再把名字改回來。
重視優先級設置
Java 採用的是搶佔式調度模型,高優先級的任務能先佔用 CPU,若是咱們想讓某個任務先完成,咱們能夠給它設置一個較高的優先級。
設置的方式就是經過 android.os.Process.setThreadPriority(priority),這個 priority 的值越小,優先級就越高,它的取值範圍在 -20~19。
在這一節,咱們會介紹 Android 中經常使用的 7 種異步方式:Thread、HandlerThread、IntentService、AsyncTask、線程池、RxJava 和 Kotlin 協程。
異步指的是代碼不是按照咱們寫的順序來執行的,除了多線程,像是 OnClickListener 中的代碼也算是異步執行的。
在編寫異步代碼時,要注意的是有可能寫出回調地獄,回調地獄代碼可能過兩天後你本身看本身寫的代碼都不會知道是幹什麼用的,好比下面這樣的。
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendRequest(request, new Callback() {
public void onSuccess(Response response) {
handler.post(new Runnable() {
@Override
public void run() {
updateUI(response);
}
})
}
})
}
});
複製代碼
直接建立 Thread 是最簡單的異步方式,可是使用這種方式除了方便簡單以外,沒有任何其餘優點。
並且使用這種方式有不少缺點,好比說不容易被複用,致使頻繁建立和銷燬線程的開銷大。
假如咱們要執行一個定時任務,直接建立 Thread 雖然也能實現,可是比較麻煩。
HandlerThread 本質上也是一個 Thread,可是它自帶了消息循環。
HandlerThread 內部是以串行的方式執行任務,它比較適合須要長時間執行,不斷從隊列中取出任務執行的場景。
IntentService 是 Service 組件的子類,它的內部有一個 HandlerThread,因此它具有了 HandlerThread 的特性。
它有兩點優點,第一點是相對於 Service 來講,IntenService 的執行是在工做線程而不是主線程。
第二點是它是一個 Service,若是應用使用了 Service,會提升應用的優先級,這樣就不容易被系統幹掉。
AsyncTask 是 Android 提供的異步工具類,它的內部實現使用了線程池,使用 AsyncTask 的好處就是不用咱們本身處理線程切換。
使用 AsyncTask 要注意它在不一樣版本的實現不一致,但這個不一致是在 API 14 如下的,而咱們如今大部分應用的適配都是在 15 及以上,因此這個問題基本上已經沒有了。
使用線程池執行異步任務有下面兩個優勢。
易於複用
經過線程池建立的線程容易複用,這樣就避免了線程頻繁建立和銷燬的開銷。
功能強大
線程池提供了幾個強大的功能,好比定時、任務隊列、併發數控制等。
咱們能夠經過 Executors 建立線程池,當 Executors 不能知足咱們的須要時,咱們能夠自定義 ThreadPoolExecutor 實現知足咱們須要的線程池。
經過下面的 ThreadPoolUtils,各個業務線使用線程時能夠經過這個類直接獲取全局線程池。
將線程池的線程數固定爲 5 個,能夠避免直接建立線程致使線程數過多。
經過 ThreadFactory,咱們能夠在建立線程時設置名字,這樣能避免沒法定位問題到出問題的線程。
private static ExecutorService sService = Executors.newFixedThreadPool(5,
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("ThreadPoolUtils");
return thread;
}
});
複製代碼
下面這段代碼是在執行任務前把線程的名字改掉,而且在任務執行完畢後把線程的名字改回來,這樣就能達到一個複用的效果。
public void executeTask() {
ThreadPoolUtils.getService().execute(new Runnable() {
@Override
public void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName("newName");
System.out.println("執行任務");
System.out.println("任務執行完畢");
Thread.currentThread().setName(oldName);
}
});
}
複製代碼
RxJava 是一個異步框架,在這裏咱們主要關注它的基本用法、異常和取消的處理。
RxJava 根據任務類型的不一樣提供了不一樣的線程池,對於 I/O 密集型任務,好比網絡請求,它提供了 I/O 線程池。
對於 CPU 密集型任務,它提供了 CPU 任務專用的線程池,也就是 Schdulers.computation()。
若是咱們項目集成了 RxJava,咱們可使用 RxJava 的線程池。
對於 12.1 小節中的代碼,使用 RxJava 寫的話是下面這樣的。
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendRequest(request)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Response>() {
@Override
public void accept(Response response) throws Exception {
updateUI(response);
}
});
}
});
複製代碼
而使用了 Lambda 表達式後,上面的代碼就變成了下面這樣。
btn.setOnClickListener(v -> sendRequest(request))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> updateUI(response));
複製代碼
可是這兩段代碼是有潛在隱患的,這個隱患是由於直接使用 Consumer 而不是 Observer,沒有對異常進行處理。
上面那段代碼,咱們能夠在 observeOn() 方法後面加上另外一個方法:onErrorReturnItem(),好比下面這樣,把異常映射成 Response。
btn.setOnClickListener(v -> sendRequest(request))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorReturnItem(t -> mapThrowableToResponse(t))
.subscribe(response -> updateUI(response));
複製代碼
另外一個辦法就是使用全局捕獲異常,捕獲到異常後上報異常。
這裏要注意的是,捕獲到的若是是 OnErrorNotImplmentedException,那咱們要上報它的 cause,由於 cause 裏面纔是真正的異常信息,好比下面這樣的。
RxJavaPlugins.setErrorHandler { e ->
report(e instanceof OnErrorNotImplmentedException ? e.getCause() : e);
Exceptions.throwIfFatal(e);
}
複製代碼
RxJava 能夠執行異步任務,異步任務就有可能出現 Acitvity 關閉後,任務還在繼續執行的狀況,這時候 Activity 就會被 Observer 持有,致使內存泄漏。
當咱們調用了 subscribe() 方法後,咱們能夠獲得一個 Disposable 對象,使用這個對象咱們能夠在頁面銷燬時取消對應的任務。
也就是咱們能夠在 Activity 中維護一個 Disposable 列表,在 onDestory() 方法中逐個取消任務。
還有一個更好的辦法,就是使用滴滴的開源框架 AutoDispose,這個框架的使用很簡單,只須要想下面這樣加上一句 as 就能夠了。
btn.setOnClickListener(v -> sendRequest(request))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorReturnItem(t -> mapThrowableToResponse(t))
.as(AutoDispose.autoDisposable(ViewScopeProvider.from(btn)))
.subscribe(response -> updateUI(response));
複製代碼
AutoDispose 的原理就是監聽傳進來的控件的生命週期,當發現這個控件的被銷燬時,每每也就意味着頁面被關閉了,這時候就能夠取消這個任務。
除了 RxJava,咱們還可使用 Kotlin 協程在 Andorid 中實現異步任務。
使用 Kotlin 協程寫出來的異步代碼,看上去跟同步代碼是很是類似的,下面是一個網絡請求的例子。
首先咱們定義一個 onClick 擴展方法,把上下文、啓動模式和協程體傳入 launch 方法中。
fun View.onClick( context: CoroutineContext = Dispatchers.Main, handler: suspend CoroutineScope.(v: View?) -> Unit
) {
setOnClickListener { v ->
GlobalScope.launch(context,CoroutineStart.DEFAULT) {
handler(v)
}
}
}
複製代碼
而後讓一個按鈕調用這個方法,而且發起網絡請求。
btn.onClick {
val request = Request()
val response = async { sendRequest(request) }.await()
updateUI(response)
}
複製代碼
上面這段代碼看上去是同步執行的,可是實際上 async {} 中的代碼是異步執行的,而且在返回了 Response 以後 updateUI() 方法纔會被執行。
使用 Kotlin 協程和 RxJava 的做用同樣,都是執行異步任務,也都須要注意任務的取消,避免內存泄漏,下面咱們就來看下怎麼取消 Kotlin 協程執行的異步任務。
對於上面這個例子,咱們能夠借鑑 AutoDispose 的思路,監聽 View 的生命週期,在 View 銷燬時取消異步任務。
使用 Kotlin 協程執行任務時咱們能夠得到一個 Job 對象,經過這個對象咱們能夠取消對應的任務。
首先咱們定義一個監聽 View 聲明週期的類 AutoDisposableJob,再定義一個 Job 類的擴展函數 autoDispose()。
class AutoDisposableJob(
private val view: View,
private val wrapped: Job
) : Job by wrapped, View.OnAttachStateChangeListener {
init {
if (ViewCompat.isAttachedToWindow(view)) {
view.addOnAttachStateChangeListener(this)
} else {
cancel()
}
invokeOnCompletion {
view.removeOnAttachStateChangeListener(this)
}
}
override fun onViewDetachedFromWindow(v: View?) {
cancel()
view.removeOnAttachStateChangeListener(this)
}
override fun onViewAttachedToWindow(v: View?) = Unit
}
fun Job.autoDispose(view: View) = AutoDisposableJob(view, this)
複製代碼
而後再在 onClick() 方法中調用 autoDispose() 擴展方法。
fun View.onClick( context: CoroutineContext = Dispatchers.Main, handler: suspend CoroutineScope.(v: View?) -> Unit
) {
setOnClickListener { v ->
GlobalScope.launch(context,CoroutineStart.DEFAULT) {
handler(v)
}.autoDispose(v)
}
}
複製代碼