JAVA-多線程

Java多線程學習筆記(一)——多線程實現和安全問題

1. 什麼是線程

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

2. 線程的狀態

1)、新建狀態(New):新創建了一個線程對象。
2)、就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。
3)、運行狀態(Running):就緒狀態的線程獲取了CPU,執行run()方法。
4)、阻塞狀態(Blocked):阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的情況分三種:
  (1)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。
  (2)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。
  (3)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
5)、死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

當調用start方法的時候,該線程就進入就緒狀態。等待CPU進行調度執行,此時還沒有真正執行線程。
當調用run方法的時候,是已經被CPU進行調度,執行線程的主要任務。

3. 線程的創建

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

4. 線程中斷

一般情況下,線程不執行完任務不會退出,但是在有些場景下,我們需要手動控制線程中斷結束任務,Java中有提供線程中斷機制相關的Api,每個線程都一個狀態位用於標識當前線程對象是否是中斷狀態
public boolean isInterrupted() //判斷中斷標識位是否是true,不會改變標識位public void interrupt() //將中斷標識位設置爲truepublic 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.更好的做法是,不捕獲異常,直接拋出給調用者處理,這樣更靈活

5. Thread爲什麼不能用stop方法停止線程

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

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

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

。。。

7. volatile關鍵字

volatile爲實例域的同步訪問提供了免鎖機制,如果聲明一個域爲volatile,那麼編譯器和虛擬機就直到該域可能被另一個線程併發更新

8. java內存模型

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

9. 原子性 可見性 有序性

原子性:對基本數據類型的讀取和賦值操作是原子性操作,這些操作不可被中斷,是一步到位的,例如x=3是原子性操作,而y = x就不是,它包含兩步:第一讀取x,第二將x寫入工作內存;x++也不是原子性操作,它包含三部,第一,讀取x,第二,對x加1,第三,寫入內存。原子性操作的類如:AtomicInteger AtomicBoolean AtomicLong AtomicReference
可見性:指線程之間的可見性,既一個線程修改的狀態對另一個線程是可見的。volatile修飾可以保證可見性,它會保證修改的值會立即被更新到主存,所以對其他線程是可見的,普通的共享變量不能保證可見性,因爲被修改後不會立即寫入主存,何時被寫入主存是不確定的,所以其他線程去讀取的時候可能讀到的還是舊值
有序性:Java中的指令重排序(包括編譯器重排序和運行期重排序)可以起到優化代碼的作用,但是在多線程中會影響到併發執行的正確性,使用volatile可以保證有序性,禁止指令重排
volatile可以保證可見性 有序性,但是無法保證原子性,在某些情況下可以提供優於鎖的性能和伸縮性,替代sychronized關鍵字簡化代碼,但是要嚴格遵循使用條件。

10. 線程池ThreadPoolExecutor

線程池的工作原理:線程池可以減少創建和銷燬線程的次數,從而減少系統資源的消耗,當一個任務提交到線程池時
a. 首先判斷核心線程池中的線程是否已經滿了,如果沒滿,則創建一個核心線程執行任務,否則進入下一步
b. 判斷工作隊列是否已滿,沒有滿則加入工作隊列,否則執行下一步
c. 判斷線程數是否達到了最大值,如果不是,則創建非核心線程執行任務,否則執行飽和策略,默認拋出異常

11. 線程池的種類

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

12. 線程同步機制與原理,舉例說明

爲什麼需要線程同步?當多個線程操作同一個變量的時候,存在這個變量何時對另一個線程可見的問題,也就是可見性。每一個線程都持有主存中變量的一個副本,當他更新這個變量時,首先更新的是自己線程中副本的變量值,然後會將這個值更新到主存中,但是是否立即更新以及更新到主存的時機是不確定的,這就導致當另一個線程操作這個變量的時候,他從主存中讀取的這個變量還是舊的值,導致兩個線程不同步的問題。線程同步就是爲了保證多線程操作的可見性和原子性,比如我們用synchronized關鍵字包裹一端代碼,我們希望這段代碼執行完成後,對另一個線程立即可見,另一個線程再次操作的時候得到的是上一個線程更新之後的內容,還有就是保證這段代碼的原子性,這段代碼可能涉及到了好幾部操作,我們希望這好幾步的操作一次完成不會被中間打斷,鎖的同步機制就可以實現這一點。一般說的synchronized用來做多線程同步功能,其實synchronized只是提供多線程互斥,而對象的wait()和notify()方法才提供線程的同步功能。JVM通過Monitor對象實現線程同步,當多個線程同時請求synchronized方法或塊時,monitor會設置幾個虛擬邏輯數據結構來管理這些多線程。新請求的線程會首先被加入到線程排隊隊列中,線程阻塞,當某個擁有鎖的線程unlock之後,則排隊隊列裏的線程競爭上崗(synchronized是不公平競爭鎖,下面還會講到)。如果運行的線程調用對象的wait()後就釋放鎖並進入wait線程集合那邊,當調用對象的notify()或notifyall()後,wait線程就到排隊那邊。這是大致的邏輯。

13. 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)

14. 爲什麼HashMap線程不安全(hash碰撞與擴容導致)

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

15. 進程線程的區別

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

16. Binder的內存拷貝過程

相比其他的IPC通信,比如消息機制、共享內存、管道、信號量等,Binder僅需一次內存拷貝,即可讓目標進程讀取到更新數據,同共享內存一樣相當高效,其他的IPC通信機制大多需要2次內存拷貝。Binder內存拷貝的原理爲:進程A爲Binder客戶端,在IPC調用前,需將其用戶空間的數據拷貝到Binder驅動的內核空間,由於進程B在打開Binder設備(/dev/binder)時,已將Binder驅動的內核空間映射(mmap)到自己的進程空間,所以進程B可以直接看到Binder驅動內核空間的內容改動

17. 傳統IPC機制的通信原理(2次內存拷貝)

1.發送方進程通過系統調用(copy_from_user)將要發送的數據存拷貝到內核緩存區中。
2.接收方開闢一段內存空間,內核通過系統調用(copy_to_user)將內核緩存區中的數據拷貝到接收方的內存緩存區。
種傳統IPC機制存在2個問題:
1.需要進行2次數據拷貝,第1次是從發送方用戶空間拷貝到內核緩存區,第2次是從內核緩存區拷貝到接收方用戶空間。
2.接收方進程不知道事先要分配多大的空間來接收數據,可能存在空間上的浪費。

18. Java內存模型(記住堆棧是內存分區,不是模型)

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

19. 類的加載過程

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

20. 什麼情況下會觸發類的初始化

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的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則先觸發其類初始化;

21. 雙親委託模式

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

22. 雙親委託模式的好處

1.避免重複加載,如果已經加載過一次Class,則不需要再次加載,而是直接讀取已經加載的Class
2.更加安全,確保,java核心api中定義類型不會被隨意替換,比如,採用雙親委託模式可以使得系統在Java虛擬機啓動時舊加載了String類,也就無法用自定義的String類來替換系統的String類,這樣便可以防止核心API庫被隨意篡改。

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

死鎖的四個必要條件
1.互斥條件:一個資源每次只能被一個進程使用
2.請求與保持條件:進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源 已被其他進程佔有,此時請求進程被阻塞,但對自己已獲得的資源保持不放。
3.不可剝奪條件:進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,即只能 由獲得該資源的進程自己來釋放(只能是主動釋放)。
4.循環等待條件: 若干進程間形成首尾相接循環等待資源的關係
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
避免死鎖的方法:系統對進程發出每一個系統能夠滿足的資源申請進行動態檢查,並根據檢查結果決定是否分配資源,如果分配後系統可能發生死鎖,則不予分配,否則予以分配,這是一種保證系統不進入死鎖狀態的動態策略。
在資源的動態分配過程中,用某種方法去防止系統進入不安全狀態,從而避免發生死鎖。 一般來說互斥條件是無法破壞的,所以在預防死鎖時主要從其他三個方面入手 :
(1)破壞請求和保持條件:在系統中不允許進程在已獲得某種資源的情況下,申請其他資源,即要想出一個辦法,阻止進程在持有資源的同時申請其它資源。
方法一:在所有進程開始運行之前,必須一次性的申請其在整個運行過程中所需的全部資源,
方法二:要求每個進程提出新的資源申請前,釋放它所佔有的資源
(2)破壞不可搶佔條件:允許對資源實行搶奪。
方式一:如果佔有某些資源的一個進程進行進一步資源請求被拒絕,則該進程必須釋放它最初佔有的資源,如果有必要,可再次請求這些資源和另外的資源。
方式二:如果一個進程請求當前被另一個進程佔有的資源,則操作系統可以搶佔另一個進程,要求它釋放資源,只有在任意兩個進程的優先級都不相同的條件下,該方法才能預防死鎖。
(3)破壞循環等待條件
對系統所有資源進行線性排序並賦予不同的序號,這樣我們便可以規定進程在申請資源時必須按照序號遞增的順序進行資源的申請,當以後要申請時需檢查要申請的資源的編號大於當前編號時,才能進行申請。
利用銀行家算法避免死鎖:
所謂銀行家算法,是指在分配資源之前先看清楚,資源分配後是否會導致系統死鎖。如果會死鎖,則不分配,否則就分配。
按照銀行家算法的思想,當進程請求資源時,系統將按如下原則分配系統資源:

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

25. 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

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

ListView採用的是RecyclerBin的回收機制在一些輕量級的List顯示時效率更高。
27. HashMap如何保證元素均勻分佈
hash & (length-1)
通過Key值的hashCode值和hashMap長度-1做與運算
hashmap中的元素,默認情況下,數組大小爲16,也就是2的4次方,如果要自定義HashMap初始化數組長度,也要設置爲2的n次方大小,因爲這樣效率最高。因爲當數組長度爲2的n次冪的時候,不同的key算出的index相同的機率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的機率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章