2019Android多執行緒總結

程式設計師大咖發表於2019-03-07

640?wx_fmt=gif

640?wx_fmt=jpeg

Linux程式設計點選右側關注,免費入門到精通!640?wx_fmt=jpeg


作者丨雨林沐風rzm
https://www.jianshu.com/p/2dff860ec73d


640?wx_fmt=gif1.什麼是執行緒


執行緒就是程式中執行的多個子任務,是作業系統呼叫的最小單元


640?wx_fmt=gif2.執行緒的狀態


New:新建狀態,new出來,還沒有呼叫start


Runnable:可執行狀態,呼叫start進入可執行狀態,可能執行也可能沒有執行,取決於作業系統的排程


Blocked:阻塞狀態,被鎖阻塞,暫時不活動,阻塞狀態是執行緒阻塞在進入synchronized關鍵字修飾的方法或程式碼塊(獲取鎖)時的狀態。


Waiting:等待狀態,不活動,不執行任何程式碼,等待執行緒排程器排程,wait sleep
Timed Waiting:超時等待,在指定時間自行返回


Terminated:終止狀態,包括正常終止和異常終止


2.執行緒的建立


a.繼承Thread重寫run方法


b.實現Runnable重寫run方法


c.實現Callable重寫call方法


實現Callable和實現Runnable類似,但是功能更強大,具體表現在


a.可以在任務結束後提供一個返回值,Runnable不行


b.call方法可以丟擲異常,Runnable的run方法不行


c.可以通過執行Callable得到的Fulture物件監聽目標執行緒呼叫call方法的結果,得到返回值,(fulture.get(),呼叫後會阻塞,直到獲取到返回值)


640?wx_fmt=gif3.執行緒中斷


一般情況下,執行緒不執行完任務不會退出,但是在有些場景下,我們需要手動控制執行緒中斷結束任務,Java中有提供執行緒中斷機制相關的Api,每個執行緒都一個狀態位用於標識當前執行緒物件是否是中斷狀態


public boolean isInterrupted() //判斷中斷標識位是否是true,不會改變標識位
public void interrupt()  //將中斷標識位設定為true
public static boolean interrupted() //判斷當前執行緒是否被中斷,並且該方法呼叫結束的時候會清空中斷標識位


需要注意的是interrupt()方法並不會真的中斷執行緒,它只是將中斷標識位設定為true,具體是否要中斷由程式來判斷,如下,只要執行緒中斷標識位為false,也就是沒有中斷就一直執行執行緒方法


new Thread(new Runnable(){
      while(!Thread.currentThread().isInterrupted()){
              //執行執行緒方法
      }
}).start();


前邊我們提到了執行緒的六種狀態,New Runnable Blocked Waiting Timed Waiting Terminated,那麼在這六種狀態下呼叫執行緒中斷的程式碼會怎樣呢,New和Terminated狀態下,執行緒不會理會執行緒中斷的請求,既不會設定標記位,在Runnable和Blocked狀態下呼叫interrupt會將標誌位設定位true,在Waiting和Timed Waiting狀態下會發生InterruptedException異常,針對這個異常我們如何處理?


1.在catch語句中通過interrupt設定中斷狀態,因為發生中斷異常時,中斷標誌位會被複位,我們需要重新將中斷標誌位設定為true,這樣外界可以通過這個狀態判斷是否需要中斷執行緒


try{
    ....
}catch(InterruptedException e){
    Thread.currentThread().interrupt();
}


2.更好的做法是,不捕獲異常,直接丟擲給呼叫者處理,這樣更靈活


640?wx_fmt=gifThread為什麼不能用stop方法停止執行緒


從SUN的官方文件可以得知,呼叫Thread.stop()方法是不安全的,這是因為當呼叫Thread.stop()方法時,會發生下面兩件事:


1.即刻丟擲ThreadDeath異常,線上程的run()方法內,任何一點都有可能丟擲ThreadDeath Error,包括在catch或finally語句中。


2.釋放該執行緒所持有的所有的鎖。呼叫thread.stop()後導致了該執行緒所持有的所有鎖的突然釋放,那麼被保護資料就有可能呈現不一致性,其他執行緒在使用這些被破壞的資料時,有可能導致一些很奇怪的應用程式錯誤。


640?wx_fmt=gif4.重入鎖與條件物件,同步方法和同步程式碼塊


。。。


640?wx_fmt=gif5.volatile關鍵字


volatile為例項域的同步訪問提供了免鎖機制,如果宣告一個域為volatile,那麼編譯器和虛擬機器就直到該域可能被另一個執行緒併發更新


java記憶體模型


堆記憶體是被所有執行緒共享的執行時記憶體區域,存在可見性的問題。執行緒之間共享變數儲存在主存中,每個執行緒都有一個私有的本地記憶體,本地記憶體儲存了該執行緒共享變數的副本(本地記憶體是一個抽象概念,並不真實存在),兩個執行緒要通訊的話,首先A執行緒把本地記憶體更新過的共享變數更新到主存中,然後B執行緒去主存中讀取A執行緒更新過的共享變數,也就是說假設執行緒A執行了i = 1這行程式碼更新主執行緒變數i的值,會首先在自己的工作執行緒中堆變數i進行賦值,然後再寫入主存當中,而不是直接寫入主存


原子性 可見性 有序性


原子性:對基本資料型別的讀取和賦值操作是原子性操作,這些操作不可被中斷,是一步到位的,例如x=3是原子性操作,而y = x就不是,它包含兩步:第一讀取x,第二將x寫入工作記憶體;x++也不是原子性操作,它包含三部,第一,讀取x,第二,對x加1,第三,寫入記憶體。原子性操作的類如:AtomicInteger AtomicBoolean AtomicLong AtomicReference


可見性:指執行緒之間的可見性,既一個執行緒修改的狀態對另一個執行緒是可見的。volatile修飾可以保證可見性,它會保證修改的值會立即被更新到主存,所以對其他執行緒是可見的,普通的共享變數不能保證可見性,因為被修改後不會立即寫入主存,何時被寫入主存是不確定的,所以其他執行緒去讀取的時候可能讀到的還是舊值

有序性:Java中的指令重排序(包括編譯器重排序和執行期重排序)可以起到優化程式碼的作用,但是在多執行緒中會影響到併發執行的正確性,使用volatile可以保證有序性,禁止指令重排


volatile可以保證可見性 有序性,但是無法保證原子性,在某些情況下可以提供優於鎖的效能和伸縮性,替代sychronized關鍵字簡化程式碼,但是要嚴格遵循使用條件。


640?wx_fmt=gif執行緒池ThreadPoolExecutor


執行緒池的工作原理:執行緒池可以減少建立和銷燬執行緒的次數,從而減少系統資源的消耗,當一個任務提交到執行緒池時


a. 首先判斷核心執行緒池中的執行緒是否已經滿了,如果沒滿,則建立一個核心執行緒執行任務,否則進入下一步


b. 判斷工作佇列是否已滿,沒有滿則加入工作佇列,否則執行下一步


c. 判斷執行緒數是否達到了最大值,如果不是,則建立非核心執行緒執行任務,否則執行飽和策略,預設丟擲異常


640?wx_fmt=gif執行緒池的種類


1.FixedThreadPool:可重用固定執行緒數的執行緒池,只有核心執行緒,沒有非核心執行緒,核心執行緒不會被回收,有任務時,有空閒的核心執行緒就用核心執行緒執行,沒有則加入佇列排隊


2.SingleThreadExecutor:單執行緒執行緒池,只有一個核心執行緒,沒有非核心執行緒,當任務到達時,如果沒有執行執行緒,則建立一個執行緒執行,如果正在執行則加入佇列等待,可以保證所有任務在一個執行緒中按照順序執行,和FixedThreadPool的區別只有數量


3.CachedThreadPool:按需建立的執行緒池,沒有核心執行緒,非核心執行緒有Integer.MAX_VALUE個,每次提交


任務如果有空閒執行緒則由空閒執行緒執行,
沒有空閒執行緒則建立新的執行緒執行,適用於大量的需要立即處理的並且耗時較短的任務


4.ScheduledThreadPoolExecutor:繼承自ThreadPoolExecutor,用於延時執行任務或定期執行任務,核心執行緒數固定,執行緒總數為Integer.MAX_VALUE


640?wx_fmt=gif執行緒同步機制與原理,舉例說明


為什麼需要執行緒同步?當多個執行緒操作同一個變數的時候,存在這個變數何時對另一個執行緒可見的問題,也就是可見性。每一個執行緒都持有主存中變數的一個副本,當他更新這個變數時,首先更新的是自己執行緒中副本的變數值,然後會將這個值更新到主存中,但是是否立即更新以及更新到主存的時機是不確定的,這就導致當另一個執行緒操作這個變數的時候,他從主存中讀取的這個變數還是舊的值,導致兩個執行緒不同步的問題。執行緒同步就是為了保證多執行緒操作的可見性和原子性,比如我們用synchronized關鍵字包裹一端程式碼,我們希望這段程式碼執行完成後,對另一個執行緒立即可見,另一個執行緒再次操作的時候得到的是上一個執行緒更新之後的內容,還有就是保證這段程式碼的原子性,這段程式碼可能涉及到了好幾部操作,我們希望這好幾步的操作一次完成不會被中間打斷,鎖的同步機制就可以實現這一點。


一般說的synchronized用來做多執行緒同步功能,其實synchronized只是提供多執行緒互斥,而物件的wait()和notify()方法才提供執行緒的同步功能。JVM通過Monitor物件實現執行緒同步,當多個執行緒同時請求synchronized方法或塊時,monitor會設定幾個虛擬邏輯資料結構來管理這些多執行緒。新請求的執行緒會首先被加入到執行緒排隊佇列中,執行緒阻塞,當某個擁有鎖的執行緒unlock之後,則排隊佇列裡的執行緒競爭上崗(synchronized是不公平競爭鎖,下面還會講到)。如果執行的執行緒呼叫物件的wait()後就釋放鎖並進入wait執行緒集合那邊,當呼叫物件的notify()或notifyall()後,wait執行緒就到排隊那邊。這是大致的邏輯。


640?wx_fmt=gifarrayList與linkedList的讀寫時間複雜度


(1)ArrayList:ArrayList是一個泛型類,底層採用陣列結構儲存物件。陣列結構的優點是便於對集合進行快速的隨機訪問,即如果需要經常根據索引位置訪問集合中的物件,使用由ArrayList類實現的List集合的效率較好。陣列結構的缺點是向指定索引位置插入物件和刪除指定索引位置物件的速度較慢,並且插入或刪除物件的索引位置越小效率越低,原因是當向指定的索引位置插入物件時,會同時將指定索引位置及之後的所有物件相應的向後移動一位。


(2)LinkedList:LinkedList是一個泛型類,底層是一個雙向連結串列,所以它在執行插入和刪除操作時比ArrayList更加的高效,但也因為連結串列的資料結構,所以在隨機訪問方面要比ArrayList差。


ArrayList 是線性表(陣列)


get() 直接讀取第幾個下標,複雜度 O(1)


add(E) 新增元素,直接在後面新增,複雜度O(1)


add(index, E) 新增元素,在第幾個元素後面插入,後面的元素需要向後移動,複雜度O(n)


remove()刪除元素,後面的元素需要逐個移動,複雜度O(n)


LinkedList 是連結串列的操作


get() 獲取第幾個元素,依次遍歷,複雜度O(n)


add(E) 新增到末尾,複雜度O(1)


add(index, E) 新增第幾個元素後,需要先查詢到第幾個元素,直接指標指向操作,複雜度O(n)


remove()刪除元素,直接指標指向操作,複雜度O(1)


640?wx_fmt=gif為什麼HashMap執行緒不安全(hash碰撞與擴容導致)


HashMap的底層儲存結構是一個Entry陣列,每個Entry又是一個單連結串列,一旦發生Hash衝突的的時候,HashMap採用拉鍊法解決碰撞衝突,因為hashMap的put方法不是同步的,所以他的擴容方法也不是同步的,在擴容過程中,會新生成一個新的容量的陣列,然後對原陣列的所有鍵值對重新進行計算和寫入新的陣列,之後指向新生成的陣列。當多個執行緒同時檢測到hashmap需要擴容的時候就會同時呼叫resize操作,各自生成新的陣列並rehash後賦給該map底層的陣列table,結果最終只有最後一個執行緒生成的新陣列被賦給table變數,其他執行緒的均會丟失。而且當某些執行緒已經完成賦值而其他執行緒剛開始的時候,就會用已經被賦值的table作為原始陣列,這樣也會有問題。擴容的時候 可能會引發連結串列形成環狀結構


640?wx_fmt=gif程式執行緒的區別


1.地址空間:同一程式的執行緒共享本程式的地址空間,而程式之間則是獨立的地址空間。


2.資源擁有:同一程式內的執行緒共享本程式的資源如記憶體、I/O、cpu等,但是程式之間的資源是獨立的。


3.一個程式崩潰後,在保護模式下不會對其他程式產生影響,但是一個執行緒崩潰整個程式都死掉。所以多程式要比多執行緒健壯。


4.程式切換時,消耗的資源大,效率不高。所以涉及到頻繁的切換時,使用執行緒要好於程式。同樣如果要求同時進行並且又要共享某些變數的併發操作,只能用執行緒不能用程式


5.執行過程:每個獨立的程式程有一個程式執行的入口、順序執行序列和程式入口。但是執行緒不能獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制。


6.執行緒是處理器排程的基本單位,但是程式不是。


7.兩者均可併發執行。


640?wx_fmt=gifBinder的記憶體拷貝過程


相比其他的IPC通訊,比如訊息機制、共享記憶體、管道、訊號量等,Binder僅需一次記憶體拷貝,即可讓目標程式讀取到更新資料,同共享記憶體一樣相當高效,其他的IPC通訊機制大多需要2次記憶體拷貝。Binder記憶體拷貝的原理為:程式A為Binder客戶端,在IPC呼叫前,需將其使用者空間的資料拷貝到Binder驅動的核心空間,由於程式B在開啟Binder裝置(/dev/binder)時,已將Binder驅動的核心空間對映(mmap)到自己的程式空間,所以程式B可以直接看到Binder驅動核心空間的內容改動


640?wx_fmt=gif傳統IPC機制的通訊原理(2次記憶體拷貝)


1.傳送方程式通過系統呼叫(copy_from_user)將要傳送的資料存拷貝到核心快取區中。


2.接收方開闢一段記憶體空間,核心通過系統呼叫(copy_to_user)將核心快取區中的資料拷貝到接收方的記憶體快取區。


種傳統IPC機制存在2個問題:


1.需要進行2次資料拷貝,第1次是從傳送方使用者空間拷貝到核心快取區,第2次是從核心快取區拷貝到接收方使用者空間。


2.接收方程式不知道事先要分配多大的空間來接收資料,可能存在空間上的浪費。


640?wx_fmt=gifJava記憶體模型(記住堆疊是記憶體分割槽,不是模型)


Java記憶體模型(即Java Memory Model,簡稱JMM)本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,通過這組規範定義了程式中各個變數(包括例項欄位,靜態欄位和構成陣列物件的元素)的訪問方式。由於JVM執行程式的實體是執行緒,而每個執行緒建立時JVM都會為其建立一個工作記憶體(有些地方稱為棧空間),用於儲存執行緒私有的資料,而Java記憶體模型中規定所有變數都儲存在主記憶體,主記憶體是共享記憶體區域,所有執行緒都可以訪問,但執行緒對變數的操作(讀取賦值等)必須在工作記憶體中進行,首先要將變數從主記憶體拷貝的自己的工作記憶體空間,然後對變數進行操作,操作完成後再將變數寫回主記憶體,不能直接操作主記憶體中的變數,工作記憶體中儲存著主記憶體中的變數副本拷貝,前面說過,工作記憶體是每個執行緒的私有資料區域,因此不同的執行緒間無法訪問對方的工作記憶體,執行緒間的通訊(傳值)必須通過主記憶體來完成


640?wx_fmt=gif類的載入過程


類載入過程主要包含載入、驗證、準備、解析、初始化、使用、解除安裝七個方面,下面一一闡述。


1.載入:獲取定義此類的二進位制位元組流,生成這個類的java.lang.Class物件


2.驗證:保證Class檔案的位元組流包含的資訊符合JVM規範,不會給JVM造成危害


3.準備:準備階段為變數分配記憶體並設定類變數的初始化


4.解析:解析過程是將常量池內的符號引用替換成直接引用


5.初始化:不同於準備階段,本次初始化,是根據程式設計師通過程式制定的計劃去初始化類的變數和其他資源。這些資源有static{}塊,建構函式,父類的初始化等


6.使用:使用過程就是根據程式定義的行為執行


7.解除安裝:解除安裝由GC完成。


640?wx_fmt=gif什麼情況下會觸發類的初始化


1.遇到new,getstatic,putstatic,invokestatic這4條指令;


2.使用java.lang.reflect包的方法對類進行反射呼叫;


3.初始化一個類的時候,如果發現其父類沒有進行過初始化,則先初始化其父類(注意!如果其父類是介面的話,則不要求初始化父類);


4.當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main方法的那個類),虛擬機器會先初始化這個主類;


5.當使用jdk1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行過初始化,則先觸發其類初始化;


雙親委託模式


類載入器查詢class所採用的是雙親委託模式,所謂雙親委託模式就是判斷該類是否已經載入,如果沒有則不是自身去查詢而是委託給父載入器進行查詢,這樣依次進行遞迴,直到委託到最頂層的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了該Class,就會直接返回,如果沒找到,則繼續依次向下查詢,如果還沒找到則最後交給自身去查詢


雙親委託模式的好處


1.避免重複載入,如果已經載入過一次Class,則不需要再次載入,而是直接讀取已經載入的Class


2.更加安全,確保,java核心api中定義型別不會被隨意替換,比如,採用雙親委託模式可以使得系統在Java虛擬機器啟動時舊載入了String類,也就無法用自定義的String類來替換系統的String類,這樣便可以防止核心API庫被隨意篡改。


死鎖的產生條件,如何避免死鎖


死鎖的四個必要條件


1.互斥條件:一個資源每次只能被一個程式使用


2.請求與保持條件:程式已經保持了至少一個資源,但又提出了新的資源請求,而該資源 已被其他程式佔有,此時請求程式被阻塞,但對自己已獲得的資源保持不放。


3.不可剝奪條件:程式所獲得的資源在未使用完畢之前,不能被其他程式強行奪走,即只能 由獲得該資源的程式自己來釋放(只能是主動釋放)。


4.迴圈等待條件: 若干程式間形成首尾相接迴圈等待資源的關係


這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。


避免死鎖的方法:系統對程式發出每一個系統能夠滿足的資源申請進行動態檢查,並根據檢查結果決定是否分配資源,如果分配後系統可能發生死鎖,則不予分配,否則予以分配,這是一種保證系統不進入死鎖狀態的動態策略。


在資源的動態分配過程中,用某種方法去防止系統進入不安全狀態,從而避免發生死鎖。 一般來說互斥條件是無法破壞的,所以在預防死鎖時主要從其他三個方面入手 :


(1)破壞請求和保持條件:在系統中不允許程式在已獲得某種資源的情況下,申請其他資源,即要想出一個辦法,阻止程式在持有資源的同時申請其它資源。


方法一:在所有程式開始執行之前,必須一次性的申請其在整個執行過程中所需的全部資源,


方法二:要求每個程式提出新的資源申請前,釋放它所佔有的資源


(2)破壞不可搶佔條件:允許對資源實行搶奪。


方式一:如果佔有某些資源的一個程式進行進一步資源請求被拒絕,則該程式必須釋放它最初佔有的資源,如果有必要,可再次請求這些資源和另外的資源。


方式二:如果一個程式請求當前被另一個程式佔有的資源,則作業系統可以搶佔另一個程式,要求它釋放資源,只有在任意兩個程式的優先順序都不相同的條件下,該方法才能預防死鎖。


(3)破壞迴圈等待條件


對系統所有資源進行線性排序並賦予不同的序號,這樣我們便可以規定程式在申請資源時必須按照序號遞增的順序進行資源的申請,當以後要申請時需檢查要申請的資源的編號大於當前編號時,才能進行申請。


利用銀行家演算法避免死鎖:


所謂銀行家演算法,是指在分配資源之前先看清楚,資源分配後是否會導致系統死鎖。
如果會死鎖,則不分配,否則就分配。


按照銀行家演算法的思想,當程式請求資源時,系統將按如下原則分配系統資源:


640?wx_fmt=gifApp啟動流程


App啟動時,AMS會檢查這個應用程式所需要的程式是否存在,不存在就會請求Zygote程式啟動需要的應用程式程式,Zygote程式接收到AMS請求並通過fock自身建立應用程式程式,這樣應用程式程式就會獲取虛擬機器的例項,還會建立Binder執行緒池(ProcessState.startThreadPool())和訊息迴圈(ActivityThread looper.loop),然後App程式,通過Binder IPC向sytem_server程式發起attachApplication請求;system_server程式在收到請求後,進行一系列準備工作後,再通過Binder IPC向App程式傳送scheduleLaunchActivity請求;App程式的binder執行緒(ApplicationThread)在收到請求後,通過handler向主執行緒傳送LAUNCH_ACTIVITY訊息;主執行緒在收到Message後,通過反射機制建立目標Activity,並回撥Activity.onCreate()等方法。到此,App便正式啟動,開始進入Activity生命週期,執行完onCreate/onStart/onResume方法,UI渲染結束後便可以看到App的主介面。


640?wx_fmt=gifAndroid單執行緒模型


Android單執行緒模型的核心原則就是:只能在UI執行緒(Main Thread)中對UI進行處理。當一個程式第一次啟動時,Android會同時啟動一個對應的 主執行緒(Main Thread),主執行緒主要負責處理與UI相關的事件,如:使用者的按鍵事件,使用者接觸螢幕的事件以及螢幕繪圖事 件,並把相關的事件分發到對應的元件進行處理。所以主執行緒通常又被叫做UI線 程。在開發Android應用時必須遵守單執行緒模型的原則:


 Android UI操作並不是執行緒安全的並且這些操作必須在UI執行緒中執行。


Android的單執行緒模型有兩條原則:


1.不要阻塞UI執行緒。


2.不要在UI執行緒之外訪問Android UI toolkit(主要是這兩個包中的元件:android.widget and android.view


RecyclerView在很多方面能取代ListView,Google為什麼沒把ListView劃上一條過時的橫線?


ListView採用的是RecyclerBin的回收機制在一些輕量級的List顯示時效率更高。


HashMap如何保證元素均勻分佈


hash & (length-1)


通過Key值的hashCode值和hashMap長度-1做與運算


hashmap中的元素,預設情況下,陣列大小為16,也就是2的4次方,如果要自定義HashMap初始化陣列長度,也要設定為2的n次方大小,因為這樣效率最高。因為當陣列長度為2的n次冪的時候,不同的key算出的index相同的機率較小,那麼資料在陣列上分佈就比較均勻,也就是說碰撞的機率小,相對的,查詢的時候就不用遍歷某個位置上的連結串列,這樣查詢效率也就較高了


 推薦↓↓↓ 

640?wx_fmt=png

?16個技術公眾號】都在這裡!

涵蓋:程式設計師大咖、原始碼共讀、程式設計師共讀、資料結構與演算法、黑客技術和網路安全、大資料科技、程式設計前端、Java、Python、Web程式設計開發、Android、iOS開發、Linux、資料庫研發、幽默程式設計師等。

640?wx_fmt=png萬水千山總是情,點個 “好看” 行不行

相關文章