大廠都愛問到多線程?27道關於多線程的總結助你一臂之力

本文主要是整理了中高級安卓須要會多線程模塊的(或者說面試被頻繁問到的內容),主要做爲參考大綱,以後會陸續更新每一個詳細部分,供你們參考,互相學習。java

面試板塊(PDF版以下):

BAT面試合集(Binder,組件化插件化,熱修復,AOP,QQ換膚,虛擬機,https,線程池原理,音視頻原理)
算法合集(Hash,KMP 等)
中小廠面試合集(內存泄漏,Handler,View,MVC.MVP.MVVM,)
大廠相關更新技術(Glide,數據庫,NDK)
面試小知識(java小知識)
設計模式(設計模式原則和分類)
數據結構(數據結構等等)
網絡編程(三次握手和四次握手,Volley,OKHttps,Retrofit)
源碼解析(屬性動畫實現原理等)
多線程解析(線程同步,進程線程)
性能優化(Webview,內存泄漏和內存溢出等)android

大廠都愛問到多線程?27道關於多線程的總結助你一臂之力
順手留下GitHub連接,須要獲取相關面試或者面試寶典核心筆記PDF等內容的能夠本身去找
https://github.com/xiangjiana/Android-MS
(VX:mm14525201314)git

一丶 什麼是線程

線程就是進程中運行的多個子任務,是操做系統調用的最小單元程序員

二丶線程的狀態

New: 新建狀態,new 出來,尚未調用 start
Runnable: 可運行狀態,調用 start 進入可運行狀態,可能運行也可能沒有運行,取決於操做系統的調度
Blocked: 阻塞狀態,被鎖阻塞,暫時不活動,阻塞狀態是線程阻塞在進入synchronized 關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態。
Waiting: 等待狀態,不活動,不運行任何代碼,等待線程調度器調度,wait sleep
Timed Waiting: 超時等待,在指定時間自行返回
Terminated: 終止狀態,包括正常終止和異常終止github

三丶線程的建立

a.繼承 Thread 重寫 run 方法
b.實現 Runnable 重寫 run 方法
c.實現 Callable 重寫 call 方法
實現 Callable 和實現 Runnable 相似,可是功能更強大,具體表如今
a.能夠在任務結束後提供一個返回值,Runnable 不行
b.call 方法能夠拋出異常,Runnable 的 run 方法不行
c.能夠經過運行 Callable 獲得的 Fulture 對象監聽目標線程調用 call 方法的結果,獲得返回值,(fulture.get(),調用後會阻塞,直到獲取到返回值)面試

四丶線程中斷

通常狀況下,線程不執行完任務不會退出,可是在有些場景下,咱們須要手動控制線程中斷結束任務,Java 中有提供線程中斷機制相關的 Api,每一個線程都一個狀態位用於標識當前線程對象是不是中斷狀態算法

public boolean isInterrupted() //判斷中斷標識位是不是 true,不會改變標
識位 public void interrupt() //將中斷標識位設置爲 truepublic static<br/>boolean interrupted() //判斷當前線程是否被中斷,而且該方法調用結束的時候會清空中斷標識位須要注意的是 interrupt()方法並不會真的中斷線程,它只是將中斷標識位設置爲 true,具體是否要中斷由程序來判斷,以下,只要線程中斷標識位爲 false,也就是沒有中斷就一直執行線程方法數據庫

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

前邊咱們提到了線程的六種狀態,New Runnable Blocked Waiting Timed WaitingTerminated,那麼在這六種狀態下調用線程中斷的代碼會怎樣呢,New 和Terminated 狀態下,線程不會理會線程中斷的請求,既不會設置標記位,在Runnable 和 Blocked 狀態下調用 interrupt 會將標誌位設置位 true,在 Waiting 和Timed Waiting 狀態下會發生 InterruptedException 異常,針對這個異常咱們如何處理?編程

1.在 catch 語句中經過 interrupt 設置中斷狀態,由於發生中斷異常時,中斷標誌位會被複位,咱們須要從新將中斷標誌位設置爲 true,這樣外界能夠經過這個狀態判斷是否須要中斷線程設計模式

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

2.更好的作法是,不捕獲異常,直接拋出給調用者處理,這樣更靈活

五丶Thread爲何不能用stop方法中止線程

從 SUN 的官方文檔能夠得知,調用 Thread.stop()方法是不安全的,這是由於當調用 Thread.stop()方法時,會發生下面兩件事:

  1. 即刻拋出 ThreadDeath 異常,在線程的 run()方法內,任何一點都有可能拋出ThreadDeath Error,包括在 catch 或 finally 語句中。
  2. 釋放該線程所持有的全部的鎖。調用 thread.stop()後致使了該線程所持有的全部鎖的忽然釋放,那麼被保護數據就有可能呈現不一致性,其餘線程在使用這些被破壞的數據時,有可能致使一些很奇怪的應用程序錯誤。

六丶 重入鎖與條件對象,同步方法和同步代碼塊

。。。

七丶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 關鍵字簡化代碼,可是要嚴格遵循使用條件。

10. 線程池 ThreadPoolExecutor

線程池的工做原理: 線程池能夠減小建立和銷燬線程的次數,從而減小系統資源的消耗,當一個任務提交到線程池時

a. 首先判斷核心線程池中的線程是否已經滿了,若是沒滿,則建立一個核心線程執行任務,不然進入下一步
b. 判斷工做隊列是否已滿,沒有滿則加入工做隊列,不然執行下一步
c. 判斷線程數是否達到了最大值,若是不是,則建立非核心線程執行任務,不然執行飽和策略,默認拋出異常

十一丶 線程池的種類

1.FixedThreadPool: 可重用固定線程數的線程池,只有核心線程,沒有非核心線程,核心線程不會被回收,有任務時,有空閒的核心線程就用核心線程執行,沒有則加入隊列排隊
2.SingleThreadExecutor: 單線程線程池,只有一個核心線程,沒有非核心線程,當任務到達時,若是沒有運行線程,則建立一個線程執行,若是正在運行則加入隊列等待,能夠保證全部任務在一個線程中按照順序執行,和 FixedThreadPool 的區別只有數量
3.CachedThreadPool: 按需建立的線程池,沒有核心線程,非核心線程有Integer.MAX_VALUE 個,每次提交任務若是有空閒線程則由空閒線程執行,沒有空閒線程則建立新的線程執行,適用於大量的須要當即處理的而且耗時較短的任務
4.ScheduledThreadPoolExecutor: 繼承自 ThreadPoolExecutor,用於延時執行任務或按期執行任務,核心線程數固定,線程總數爲Integer.MAX_VALUE

十二丶線程同步機制與原理

爲何須要線程同步?當多個線程操做同一個變量的時候,存在這個變量什麼時候對另外一個線程可見的問題,也就是可見性。每個線程都持有主存中變量的一個副本,當他更新這個變量時,首先更新的是本身線程中副本的變量值,而後會將這個值更新到主存中,可是是否當即更新以及更新到主存的時機是不肯定的這就致使當另外一個線程操做這個變量的時候,他從主存中讀取的這個變量仍是舊的值,致使兩個線程不一樣步的問題。

線程同步就是爲了保證多線程操做的可見性和原子性,好比咱們用synchronized 關鍵字包裹一端代碼,咱們但願這段碼執行完成後,對另外一個線程當即可見,另外一個線程再次操做的時候獲得的是上一個線程更新以後的內容,還有就是保證這段代碼的原子性,這段代碼可能涉及到了好幾部操做,咱們但願這好幾步的操做一次完成不會被中間打斷,鎖的同步機制就能夠實現這一點。通常說的 synchronized 用來作多線程同步功能,其實synchronized 只是提供多線程互斥,而對象的 wait()和 notify()方法才提供線程的同步功能。

JVM 經過 Monitor 對象實現線程同步,當多個線程同時請求synchronized 方法或塊時,monitor 會設置幾個虛擬邏輯數據結構來管理這些多線程。新請求的線程會首先被加入到線程排隊隊列中,線程阻塞,當某個擁有鎖的線程 unlock 以後,則排隊隊列裏的線程競爭上崗(synchronized 是不公平競爭鎖,下面還會講到)。若是運行的線程調用對象的 wait()後就釋放鎖並進入 wait線程集合那邊,當調用對象的 notify()notifyall()後,wait 線程就到排隊那邊。這是大體的邏輯。

十三丶arrayList 與 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)

十四丶 爲何HashMap 線程不安全(hash碰撞與擴容致使)

HashMap 的底層存儲結構是一個 Entry 數組,每一個 Entry 又是一個單鏈表,一旦發生 Hash 衝突的的時候,HashMap 採用拉鍊法解決碰撞衝突,由於 hashMap 的put 方法不是同步的,因此他的擴容方法也不是同步的,在擴容過程當中,會新生成一個新的容量的數組,而後對原數組的全部鍵值對從新進行計算和寫入新的數組,以後指向新生成的數組。當多個線程同時檢測到 hashmap 須要擴容的時候就會同時調用 resize 操做,各自生成新的數組並 rehash 後賦給該 map 底層的數組 table,結果最終只有最後一個線程生成的新數組被賦給 table 變量,其餘線程的均會丟失。並且當某些線程已經完成賦值而其餘線程剛開始的時候,就會用已經被賦值的 table 做爲原始數組,這樣也會有問題。擴容的時候 可能會引起鏈表造成環狀結構

十五丶 進程線程的區別

1.地址空間: 同一進程的線程共享本進程的地址空間,而進程之間則是獨立的地址空間。
2.資源擁有: 同一進程內的線程共享本進程的資源如內存、I/O、cpu 等,可是進程之間的資源是獨立的。
3.一個進程崩潰後,在保護模式下不會對其餘進程產生影響,可是一個線程崩潰整個進程都死掉。因此多進程要比多線程健壯
4.進程切換時,消耗的資源大,效率不高。因此涉及到頻繁的切換時,使用線程要好於進程。一樣若是要求同時進行而且又要共享某些變量的併發操做,只能用線程不能用進程
5.執行過程: 每一個獨立的進程程有一個程序運行的入口、順序執行序列和程序入口。可是線程不能獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
6.線程是處理器調度的基本單位,可是進程不是。
7.二者都可併發執行。

十六丶Binder 的內存拷貝過程

相比其餘的 IPC 通訊,好比消息機制、共享內存、管道、信號量等,Binder 僅需一次內存拷貝,便可讓目標進程讀取到更新數據,同共享內存同樣至關高效,其餘的 IPC 通訊機制大多須要 2 次內存拷貝。

Binder 內存拷貝的原理爲: 進程 A 爲Binder 客戶端,在 IPC 調用前,需將其用戶空間的數據拷貝到 Binder 驅動的內核空間,因爲進程 B 在打開 Binder 設備(/dev/binder)時,已將 Binder 驅動的內核空間映射(mmap)到本身的進程空間,因此進程 B 能夠直接看到 Binder 驅動內核空間的內容改動

十七丶傳統 IPC 機制的通訊原理(2次內存拷貝)

1.發送方進程經過系統調用(copy_from_user)將要發送的數據存拷貝到內核緩存區中。
2.接收方開闢一段內存空間,內核經過系統調用(copy_to_user)將內核緩存區中的數據拷貝到接收方的內存緩存區。
幾種傳統 IPC 機制存在 2 個問題:

1.須要進行 2 次數據拷貝,第 1 次是從發送方用戶空間拷貝到內核緩存區,第 2次是從內核緩存區拷貝到接收方用戶空間。
2.接收方進程不知道事先要分配多大的空間來接收數據,可能存在空間上的浪費。

十八丶Java內存模型 (記住堆棧是內存分區,不是模型)

Java 內存模型(即 Java Memory Model,簡稱 JMM)自己是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,經過這組規範定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。因爲 JVM 運行程序的實體是線程,而每一個線程建立時 JVM 都會爲其建立一個工做內存(有些地方稱爲棧空間),用於存儲線程私有的數據,而 Java 內存模型中規定全部變量都存儲在主內存,主內存是共享內存區域,全部線程均可以訪問,但線程對變量的操做(讀取賦值等)必須在工做內存中進行,首先要將變量從主內存拷貝的本身的工做內存空間,而後對變量進行操做,操做完成後再將變量寫回主內存,不能直接操做主內存中的變量,工做內存中存儲着主內存中的變量副本拷貝,前面說過,工做內存是每一個線程的私有數據區域,所以不一樣的線程間沒法訪問對方的工做內存,線程間的通訊(傳值)必須經過主內存來完成

十九丶類的加載過程

類加載過程主要包含加載、驗證、準備、解析、初始化、使用、卸載七個方面,下面一一闡述。
1.加載: 獲取定義此類的二進制字節流,生成這個類的 java.lang.Class 對象
2.驗證: 保證 Class 文件的字節流包含的信息符合 JVM 規範,不會給 JVM 形成危害
3.準備: 準備階段爲變量分配內存並設置類變量的初始化
4.解析: 解析過程是將常量池內的符號引用替換成直接引用
5.初始化: 不一樣於準備階段,本次初始化,是根據程序員經過程序制定的計劃去初始化類的變量和其餘資源。這些資源有 static{}塊,構造函數,父類的初始化等
6.使用:使用過程就是根據程序定義的行爲執行
7.卸載: 卸載由 GC 完成

二十丶 什麼狀況下會觸發類的初始化

一、 遇到 newgetstaticputstaticinvokestatic 這 4 條指令;
二、 使用 java.lang.reflect 包的方法對類進行反射調用;
三、 初始化一個類的時候,若是發現其父類沒有進行過初始化,則先初始化其父類(注意!若是其父類是接口的話,則不要求初始化父類);
四、 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含 main 方法的那個類),虛擬機會先初始化這個主類;
五、 當使用 jdk1.7 的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果 REF_getstaticREF_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)破壞循環等待條件
對系統全部資源進行線性排序並賦予不一樣的序號,這樣咱們即可以規定進程在申請資源時必須按照序號遞增的順序進行資源的申請,當之後要申請時需檢查要申請的資源的編號大於當前編號時,才能進行申請。
利用算法避免死鎖:
所謂銀行家算法,是指在分配資源以前先看清楚,資源分配後是否會致使系統死鎖。若是會死鎖,則不分配,不然就分配。按照銀行家算法的思想,當進程請求資源時,系統將按以下原則分配系統資源

二十四丶App啓動流程

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 的主界面。

二十五丶Android 單線程模型

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 相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了

順手留下GitHub連接,須要獲取相關面試等內容的能夠本身去找
https://github.com/xiangjiana/Android-MS

PDF獲取
相關文章
相關標籤/搜索