第十章 進程間的通信 之 Java/Android多線程開發(二)

(一)Java 多線程開發

1.1)線程狀態

(1)Java線程 6 種狀態
public static enum Thread.State 用來描述一個線程的狀態,包括6種狀態
注:這裏的六中狀態是JVM層面的狀態,與OS底層的線程狀態不同(OS底層線程狀態有5種,如下圖)
Java 的線程狀態傾向於描述線程,而OS的線程狀態傾向於描述CPU。
在這裏插入圖片描述
而對於Java而言,Java的線程類型都來源於Thread 類下的 State 這一內部枚舉類中所定義的狀態:

  1. NEW 新建狀態
    當用new操作符創建一個線程後,如Thread thread = new Thread(),此時線程處在新建狀態。 當一個線程處於新建狀態時,線程中的任務代碼還沒開始運行。
    這裏的開始執行具體指調用線程中start方法。(一個線程只能start一次,不能直接調用run方法,只有調用start方法纔會開啓新的執行線程,接着它會去調用run。在start之後,線程進入RUNNABLE狀態,之後還可能會繼續轉換成其它狀態。)
  2. RUNNABLE 就緒狀態(可執行狀態)
    也被稱爲“可執行狀態”。一個新創建的線程並不自動開始運行,要執行線程,必須調用線程的start()方法。當調用了線程對象的start()方法即啓動了線程,此時線程就處於就緒狀態。
    處於就緒狀態的線程並不一定立即運行run()方法,線程還必須同其他就緒線程競爭CPU,只有獲得CPU使用權纔可以運行線程。比如在單核心CPU的計算機系統中,不可能同時運行多個線程,一個時刻只能有一個線程處於運行狀態。對與多個處於就緒狀態的線程是由Java運行時系統的線程調度程序(thread scheduler)來調度執行。
    除了調用start()方法後讓線程變成就緒狀態,一個線程阻塞狀態結束後也可以變成就緒狀態,或者從運行狀態變化到就緒狀態。
    對於Java虛擬機的RUNNABLE狀態,包含OS的Ready、Running。(由於現在的時分(time-sharing)多任務(multi-task)操作系統架構通常都是用所謂的“時間分片”方式進行搶佔式輪轉調度,其中上下文切換過程很快,因此ready與running狀態切換很快,對於RUNNABLE狀態,就沒有切換的意義了。)
    以及部分waiting狀態(即OS狀態下的阻塞式I/O操作),這些狀態可統一歸納爲RUNNABLE狀態的官方定義:
    處於 runnable 狀態下的線程正在 Java 虛擬機中執行,但它可能正在等待來自於操作系統的其它資源,比如處理器或者其他I/O設備等。(CPU、硬盤、網卡等資源,若在爲線程服務,就認爲線程在"執行")
  3. BLOCKED 阻塞狀態
    線程在獲取鎖失敗時(因爲鎖被其它線程搶佔),它會被加入鎖的同步阻塞隊列,然後線程進入阻塞狀態(Blocked)。處於阻塞狀態(Blocked)的線程放棄CPU使用權,暫時停止運行。待其它線程釋放鎖之後,阻塞狀態(Blocked)的線程將在次參與鎖的競爭,如果競爭鎖成功,線程將進入就緒狀態(Runnable) 。
    注意區分BLOCKED 狀態與一般的I/O阻塞:BLOCKED狀態特指被 synchronized 塊阻塞,即是跟線程同步有關的一個狀態。
    進程同步
    線程同步機制用於解決多線程之間競爭關係——爭奪鎖。在ava 在語言級直接提供了同步的機制,也即是 synchronized 關鍵字:
synchronized(expression) {……}

它的機制是這樣的:對錶達式(expresssion)求值(值的類型須是引用類型(reference type)),獲取它所代表的對象,然後嘗試獲取這個對象的鎖:
如果能獲取鎖,則進入同步塊執行,執行完後退出同步塊,並歸還對象的鎖(異常退出也會歸還);如果不能獲取鎖,則阻塞在這裏,直到能夠獲取鎖。如果一個線程在同步塊中,則其他想進入該同步塊的進程被阻塞,處於該同步塊的Entry Set中,處於BLOCKED狀態。
BLOCKED狀態官方定義如下:

一個正在阻塞等待一個監視器鎖的線程處於這一狀態。(A thread that is blocked waiting for a monitor lock is in this state.)

包括兩種情況:
(1)進入(enter)同步塊時阻塞
一個處於 blocked 狀態的線程正在等待一個監視器鎖以進入一個同步的塊或方法。
監視器鎖用於同步訪問,以達到多線程間的互斥。所以一旦一個線程獲取鎖進入同步塊,在其出來之前,如果其它線程想進入,就會因爲獲取不到鎖而阻塞在同步塊之外,這時的狀態就是 BLOCKED。
(2)wait 之後重進入(reenter)同步塊時阻塞
一個處於 blocked 狀態的線程正在等待一個監視器鎖,在其調用 Object.wait 方法之後,以再次進入一個同步的塊或方法。
過程如下:

  • 調用 wait 方法必須在同步塊中,即是要先獲取鎖並進入同步塊,這是第一次 enter。
  • 而調用 wait 之後則會釋放該鎖,並進入此鎖的等待隊列(wait set)中。
  • 當收到其它線程的 notify 或 notifyAll 通知之後,等待線程並不能立即恢復執行,因爲停止的地方是在同步塊內,而鎖已經釋放了,所以它要重新獲取鎖才能再次進入(reenter)同步塊,然後從上次 wait 的地方恢復執行。這是第二次 enter,所以叫 reenter。
  • 但鎖並不會優先給它,該線程還是要與其它線程去競爭鎖,這一過程跟 enter 的過程其實是一樣的,因此也可能因爲鎖已經被其它線程據有而導致 BLOCKED。

這兩種情況可總結爲:當因爲獲取不到鎖而無法進入同步塊時,線程處於 BLOCKED 狀態。BLOCKED狀態可以看做特殊的WAITING,表示等待同步鎖的狀態。如果有線程長時間處於 BLOCKED 狀態,要考慮是否發生了死鎖(deadlock)的狀況。

  1. WAITING 等待狀態(條件等待狀態)
    當線程的運行條件不滿足時,通過鎖的條件等待機制(調用鎖對象的wait()或顯示鎖條件對象的await()方法)讓線程進入等待狀態(WAITING)。處於等待狀態的線程將不會被cpu執行,除非線程的運行條件得到滿足後,其可被其他線程喚醒,進入阻塞狀態(Blocked)。調用不帶超時的Thread.join()方法也會進入等待狀態。
    一個正在無限期等待另一個線程執行一個特別的動作的線程處於這一狀態。

一個線程進入 WAITING 狀態是因爲調用了以下方法:

  • 不帶時限的 Object.wait 方法
  • 不帶時限的 Thread.join 方法

然後會等其它線程執行一個特別的動作,比如:

  • 一個調用了某個對象的 Object.wait 方法的線程會等待另一個線程調用此對象的 Object.notify() 或 Object.notifyAll()。
  • 一個調用了 Thread.join 方法的線程會等待指定的線程結束。

進程協作
可以看出,WAITING狀態所涉及的不是一個線程的獨角戲,相反,它涉及多個線程,具體地講,這是多個線程間的一種協作機制。wait/notify與join都是線程間的一種協作機制。下面分別介紹wait/notify場景與join場景
(1)wait/notify場景
當獲得鎖的線程A進入同步塊後發現條件不滿足時,應該調用 wait()方法,這時線程A釋放鎖,並進入所謂的 wait set 中。這時,線程A不再活動,不再參與調度,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程A狀態即是 WAITING。
現在的問題是:線程A什麼時候才能再次活動呢?顯然,最佳的時機是當條件滿足的時候。
(此時可能存在多個類似線程A這種條件不滿足的線程無法執行,與線程B爭奪鎖資源從而導致飢餓狀態)
當另一個線程B執行動作使線程A執行條件滿足後,它還要執行一個特別的動作,也即是“通知(notify)”處於WAITING狀態的線程A,即是把它從 wait set 中釋放出來,重新進入到調度隊列(ready queue)中。
如果是 notify,則選取所通知對象的 wait set 中的一個線程釋放;
如果是 notifyAll,則釋放所通知對象的 wait set 上的全部線程。
但被通知線程A並不能立即恢復執行,因爲它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以它需要再次嘗試去獲取鎖(很可能面臨其它線程的競爭),成功後才能在當初調用 wait 方法之後的地方恢復執行。(這也即是所謂的 “reenter after calling Object.wait”,即BLOCKED狀態。)

  • 如果能獲取鎖,線程A就從 WAITING 狀態變成 RUNNABLE 狀態;
  • 否則,從 wait set 出來,又進入 entry set,線程A就從 WAITING 狀態又變成 BLOCKED 狀態。

綜上,這是一個協作機制,需要兩個具有協作關係的線程A、B分別執行wait和notify。顯然,這種協作關係的存在,線程A可以避免在條件不滿足時的盲目嘗試,也爲線程B的順利執行騰出了資源;同時,在條件滿足時,又能及時得到通知。協作關係的存在使得彼此都能受益。
這裏的協作機制也即經典的消費者-生產者問題
(2)join場景
從定義中可知,除了 wait/notify 外,調用 join 方法也會讓線程處於 WAITING 狀態。
join 的機制中並沒有顯式的 wait/notify 的調用,但可以視作是一種特殊的,隱式的 wait/notify 機制。
假如有 a,b 兩個線程,在 a 線程中執行 b.join(),相當於讓 a 去等待 b,此時 a 停止執行,等 b 執行完了,系統內部會隱式地通知 a,使 a 解除等待狀態,恢復執行。
換言之,a 等待的條件是 “b 執行完畢”,b 完成後,系統會自動通知 a。

  1. TIMED_WAITING 限時等待狀態
    限時等待是WAITING等待狀態的一種特例,主要是在時限參數和sleep方法的不同。線程在等待時我們將設定等待超時時間,如超過了我們設定的等待時間,等待線程將自動喚醒進入阻塞狀態(Blocked)或就緒狀態(Runnable) 。在調用Thread.sleep()方法、帶有超時設定的Object.wait()方法、帶有超時設定的Thread.join()方法等,線程會進入限時等待狀態(TIMED_WAITING)。
    一個正在限時等待另一個線程執行一個動作的線程處於這一狀態。
    帶指定的等待時間的等待線程所處的狀態。一個線程處於這一狀態是因爲用一個指定的正的等待時間(爲參數)調用了以下方法中的其一:
  • Thread.sleep
  • 帶時限(timeout)的 Object.wait
  • 帶時限(timeout)的 Thread.join

(1)帶參數的wait(n)
沒有參數的wait()等價於wait(0),表示線程永久等下去,等到天荒地老,除非收到通知。這種完全將再次活動的命運交給通知者可能會導致該線程永遠等下去,無法得到執行的機會(當通知者準備執行notify時因某種原因被殺死,持有的鎖也釋放,此時線程執行的條件滿足了,但等待的線程卻因收不到通知從而一直處於等待狀態)
此時可設置帶有參數的wait(1000),等待1秒,相當於等待兩個通知,取決於哪個先到:

  • 如果在1000毫秒內,線程A收到了線程B的通知而喚醒,則這個鬧鐘隨之失效;
  • 如果超過了1000毫秒還沒收到通知,則鬧鐘將線程A喚醒。

(2)sleep
進入 TIMED_WAITING 狀態的另一種常見情形是調用的 sleep 方法,單獨的線程也可以調用,不一定非要有協作關係。
這種情況下就是完全靠“自帶鬧鐘”來通知。(sleep方法不會等待協作進程的通知)
sleep方法沒有任何同步語義,與鎖無關:sleep方法不會等待協作進程的通知,當線程調用sleep方法時帶了鎖,則sleep期間鎖仍爲線程所擁有。

補充:wait 與 sleep 的區別與聯繫
wait和sleep均能使線程處於等待狀態

  • 定義
    wait方法定義在Object裏面,基於對象鎖,所有的對象都能使用
    (Java裏面每一個對象都有隱藏鎖,也叫監視器(monitor)。當一個線程進入一個synchronized方法的時候它會獲得一個當前對象的鎖。)
    sleep方法定義在Thread裏面,是基於當前線程
  • 條件
    wait必須在同步環境(synchronized方法)下使用,否則會報IllegalMonitorStateException異常
    sleep方法可在任意條件下使用
  • 功能
    wait/notify一起使用,用於線程間的通信。wait用於讓線程進入等待狀態,notify則喚醒正在等待的線程。
    sleep用於暫停當前線程的執行,它會在一定時間內釋放CPU資源給其他線程執行,超過睡眠時間則會正常喚醒。
  • 鎖的持有
    在同步環境中調用wait方法會釋放當前持有的鎖
    調用sleep則不會釋放鎖,一直持有鎖(直到睡眠結束)
  1. TERMINATED 死亡狀態
    線程執行完了(completed execution)或者因異常退出了run()方法(exited),該線程結束生命週期。

總結:

  • BLOCKED狀態和WAITING狀態對比
    BLOCKED是同步(synchronized)機制下被動阻塞等待獲取同步鎖的狀態,處於running(OS意義下)狀態的線程可通過加同步鎖(Synchronized)被動進入BLOCKED狀態。
    WAITING是異步(wait/notify)機制下主動等待條件滿足後獲取通知的狀態,處於running(OS意義下)狀態的線程可主動調用object.wait或者sleep,或者join(join內部調用的是sleep,所以可看成sleep的一種)進入WAITING狀態。
  • JVM層面進程狀態與OS層面進程狀態對比
    在這裏插入圖片描述
  • 導致線程阻塞(OS意義下的阻塞waiting狀態)的原因
    線程阻塞的特點
    線程放棄CPU的使用,暫停運行。只有等阻塞原因消除後回覆運行;或是被其他線程中斷導致該線程退出阻塞狀態,同時跑出InterruptedException.
    線程阻塞的狀態包括
    BLOCKED狀態 無法獲取同步鎖 :synchronic
    WAITING狀態(TIMED_WAITING狀態) 不滿足運行條件 :wait/notify、sleep
    RUNNABLE狀態 正在JVM中執行,佔用某個資源 :阻塞式 I/O 操作
    線程阻塞的原因
    (1)Thread.sleep(int millsecond) 調用 sleep 的線程會在一定時間內將 CPU 資源給其他線程執行,超過睡眠事件後喚醒。與是否持有同步鎖無關。進程處於 TIMED_WAITING 狀態
    (2)線程執行一段同步代碼(Synchronic)代碼,但無法獲取同步鎖:同步鎖用於實現線程同步執行,未獲得同步鎖而無法進入同步塊的線程處於 BLOCKED 狀態
    (3)線程對象調用 wait 方法,進入同步塊的線程發現運行條件不滿足,此時會釋放鎖,並釋放CPU,等待其他線程norify。此時線程處於 WAITING 狀態
    (4)執行阻塞式I/O操作,等待相關I/O設備(如鍵盤、網卡等),爲了節省CPU資源,釋放CPU。此時線程處於RUNNABLE狀態。

(2)Java線程 狀態轉換
“Java 線程狀態的改變通常只與自身顯式引入的機制有關。如果 JVM 中的線程狀態發生改變了,通常是自身機制引發的。比如 synchronize 機制有可能讓線程進入BLOCKED 狀態,sleep,wait等方法則可能讓其進入 WATING 之類的狀態。
在這裏插入圖片描述

1.2)線程控制方法

JVM充分地利用現代多核處理器的強大性能。採用異步調用線程,提高使用性能,缺點就是會造成線程不安全。爲了保證線程安全性,即確保Java內存模型的可見性、原子性和有序性。Java主要通過volatile、synchronized實現線程安全。

(1.2.1)Synchronized

synchronized 規定了同一個時刻只允許一條線程可以進入臨界區(互斥性),同時還保證了共享變量的內存可見性。此規則決定了持有同一個對象鎖的多個同步塊只能串行執行。
Java中的每個對象都可以爲鎖。

  1. 普通同步方法,鎖是當前實例對象。
  2. 靜態同步方法,鎖是當前類的class對象。
  3. 同步代碼塊,鎖是括號中的對象。

synchronized 是應用於同步問題的人工線程調度工具。Java中的每個對象都有一個監視器,來監測併發代碼的重入。在非多線程編碼時該監視器不發揮作用,反之如果在synchronized 範圍內(線程進入同步塊),監視器發揮作用,線程獲得內置鎖。內置鎖是一個互斥鎖,以爲着最多隻有一個線程能夠獲取該鎖。這個鎖由JVM自動獲取和釋放,線程進入synchronized方法時獲取該對象的鎖,synchronized方法正常返回或者拋異常而終止,JVM會自動釋放對象鎖。這裏也體現了用synchronized來加鎖的1個好處,方法拋異常的時候,鎖仍然可以由JVM來自動釋放。
wait/notify必須存在於synchronized塊中。並且,這三個關鍵字針對的是同一個監視器(某個對象的監視器)。
當某個線程wait之後,其他執行該同步快的線程可以進入該同步塊執行。
當某個線程並不持有監視器的使用權時(如上圖中5的狀態,即脫離同步塊)去wait或notify,會拋出java.lang.IllegalMonitorStateException。
在synchronized塊中去調用另一個對象的wait/notify,因爲不同對象的監視器不同,同樣會拋出此異常。
鎖的內部機制:從偏向鎖到重量級鎖
1. 對象頭和monitor
Java對象在內存中的存儲結構主要有一下三個部分:

  1. 對象頭
  2. 實例數據
  3. 填充數據

當創建一個對象時LockObject時,對象的Markword 存儲鎖的相關信息,包括指向輕量級鎖指針、指向重量級鎖指針、偏向線程ID 等。
monitor是線程私有的數據結構,每一個線程都有一個可用monitor列表,同時還有一個全局的可用列表,先來看monitor的內部
在這裏插入圖片描述

  • Owner:初始時爲NULL表示當前沒有任何線程擁有該monitor,當線程成功擁有該鎖後保存線程唯一標識,當鎖被釋放時又設置爲NULL;
  • EntryQ:關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor失敗的線程。
  • RcThis:表示blocked或waiting在該monitor上的所有線程的個數。
  • Nest:用來實現重入鎖的計數。
  • HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
  • Candidate:用來避免不必要的阻塞或等待線程喚醒,因爲每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然後因爲競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值:0表示沒有需要喚醒的線程,1表示要喚醒一個繼任線程來競爭鎖。

在 java 虛擬機中,線程一旦進入到被synchronized修飾的方法或代碼塊時,指定的鎖對象通過某些操作將對象頭中的LockWord指向monitor 的起始地址與之關聯,同時monitor 中的Owner存放擁有該鎖的線程的唯一標識,確保一次只能有一個線程執行該部分的代碼,線程在獲取鎖之前不允許執行該部分的代碼。

2. 偏向鎖
當線程執行到臨界區(critical section)時,此時會利用CAS(Compare and Swap)操作,將線程ID插入到Markword中,同時修改偏向鎖的標誌位。
此時偏向鎖標誌位爲1。
偏向鎖是jdk1.6引入的一項鎖優化,其中的“偏”是偏心的偏。它的意思就是說,這個鎖會偏向於第一個獲得它的線程,在接下來的執行過程中,假如該鎖沒有被其他線程所獲取,沒有其他線程來競爭該鎖,那麼持有偏向鎖的線程將永遠不需要進行同步操作。
也就是說:
在此線程之後的執行過程中,如果再次進入或者退出同一段同步塊代碼,並不再需要去進行加鎖或者解鎖操作,而是會做以下的步驟:
Load-and-test,也就是簡單判斷一下當前線程id是否與Markword當中的線程id是否一致.
如果一致,則說明此線程已經成功獲得了鎖,繼續執行下面的代碼.
如果不一致,則要檢查一下對象是否還是可偏向,即“是否偏向鎖”標誌位的值。
如果還未偏向,則利用CAS操作來競爭鎖,也即是第一次獲取鎖時的操作。
如果此對象已經偏向了,並且不是偏向自己,則說明存在了競爭。此時可能就要根據另外線程的情況,可能是重新偏向,也有可能是做偏向撤銷,但大部分情況下就是升級成輕量級鎖了。
即偏向鎖是針對於一個線程而言的,線程獲得鎖之後就不會進行解鎖操作,節省了很多開銷。爲什麼要這樣做呢?因爲經驗表明,其實大部分情況下,都會是同一個線程進入同一塊同步代碼塊的。這也是爲什麼會有偏向鎖出現的原因。在Jdk1.6中,偏向鎖的開關是默認開啓的,適用於只有一個線程訪問同步塊的場景。
下述代碼中,當線程訪問同步方法method1時,會在對象頭(SynchronizedTest.class對象的對象頭)和棧幀的鎖記錄中存儲鎖偏向的線程ID,下次該線程在進入method2,只需要判斷對象頭存儲的線程ID是否爲當前線程,而不需要進行CAS操作進行加鎖和解鎖(因爲CAS原子指令雖然相對於重量級鎖來說開銷比較小但還是存在非常可觀的本地延遲)。

public class SynchronizedTest {
    private static Object lock = new Object();
    public static void main(String[] args) {
        method1();
        method2();
    }
    synchronized static void method1() {}
    synchronized static void method2() {}
}

3. 輕量級鎖
當出現有兩個線程來競爭鎖的話,那麼偏向鎖就失效了,此時鎖就會膨脹,升級爲輕量級鎖。鎖撤銷升級爲輕量級鎖之後,那麼對象的Markword也會進行相應的的變化。下面先簡單描述下鎖撤銷之後,升級爲輕量級鎖的過程:

  1. 線程在自己的棧楨中創建鎖記錄 LockRecord。
  2. 將鎖對象的對象頭中的MarkWord複製到線程的剛剛創建的鎖記錄中。
  3. 將鎖記錄中的Owner指針指向鎖對象。
  4. 將鎖對象的對象頭的MarkWord替換爲指向鎖記錄的指針。

輕量級鎖主要是自旋鎖。所謂自旋,就是指當有另外一個線程來競爭鎖時,這個線程會在原地循環等待,而不是把該線程給阻塞,直到那個獲得鎖的線程釋放鎖之後,這個線程就可以馬上獲得鎖的。注意,鎖在原地循環的時候,是會消耗cpu的,就相當於在執行一個啥也沒有的for循環。所以,輕量級鎖適用於那些同步代碼塊執行的很快的場景,這樣,線程原地等待很短很短的時間就能夠獲得鎖了。自旋鎖有一些問題:
(1)如果同步代碼塊執行的很慢,需要消耗大量的時間,那麼這個時侯,其他線程在原地等待空消耗cpu。
(2)本來一個線程把鎖釋放之後,當前線程是能夠獲得鎖的,但是假如這個時候有好幾個線程都在競爭這個鎖的話,那麼有可能當前線程會獲取不到鎖,還得原地等待繼續空循環消耗cup,甚至有可能一直獲取不到鎖。
基於這個問題,我們必須給線程空循環設置一個次數,當線程超過了這個次數,我們就認爲,繼續使用自旋鎖就不適合了,此時鎖會再次膨脹,升級爲重量級鎖。

4. 重量級鎖
輕量級鎖膨脹之後,就升級爲重量級鎖了。重量級鎖是依賴對象內部的monitor鎖來實現的,而monitor又依賴操作系統的MutexLock(互斥鎖)來實現的,所以重量級鎖也被成爲互斥鎖。
主要是,當系統檢查到鎖是重量級鎖之後,會把等待想要獲得鎖的線程進行阻塞,被阻塞的線程不會消耗cup。但是阻塞或者喚醒一個線程時,都需要操作系統來幫忙,這就需要從用戶態轉換到內核態,而轉換狀態是需要消耗很多時間的,有可能比用戶執行代碼的時間還要長。
這就是說爲什麼重量級線程開銷很大的。互斥鎖(重量級鎖)也稱爲阻塞同步、悲觀鎖
因此可做個總結:
線程可以通過兩種方式鎖住一個對象:

  1. 通過膨脹一個處於無鎖狀態(狀態位001)的對象獲得該對象的鎖;
  2. 對象處於膨脹狀態(狀態位00),但LockWord指向的monitor的Owner字段爲NULL,則可以直接通過CAS原子指令嘗試將Owner設置爲自己的標識來獲得鎖。

獲取鎖(monitorenter)的大概過程:

  1. 對象處於無鎖狀態時(LockWord的值爲hashCode等,狀態位爲001),線程首先從monitor列表中取得一個空閒的monitor,初始化Nest和Owner值爲1和線程標識,一旦monitor準備好,通過CAS替換monitor起始地址到LockWord進行膨脹。如果存在其它線程競爭鎖的情況而導致CAS失敗,則回到monitorenter重新開始獲取鎖的過程即可。
  2. 對象已經膨脹,monitor中的Owner指向當前線程,這是重入鎖的情況(reentrant),將Nest加1,不需要CAS操作,效率高。
  3. 對象已經膨脹,monitor中的Owner爲NULL,此時多個線程通過CAS指令試圖將Owner設置爲自己的標識獲得鎖,競爭失敗的線程則進入第4種情況。
  4. 對象已經膨脹,同時Owner指向別的線程,在調用操作系統的重量級的互斥鎖之前自旋一定的次數,當達到一定的次數如果仍然沒有獲得鎖,則開始準備進入阻塞狀態,將rfThis值原子加1,由於在加1的過程中可能被其它線程破壞對象和monitor之間的聯繫,所以在加1後需要再進行一次比較確保lock word的值沒有被改變,當發現被改變後則要重新進行monitorenter過程。同時再一次觀察Owner是否爲NULL,如果是則調用CAS參與競爭鎖,鎖競爭失敗則進入到阻塞狀態。

釋放鎖(monitorexit)的大概過程:

  1. 檢查該對象是否處於膨脹狀態並且該線程是這個鎖的擁有者,如果發現不對則拋出異常。
  2. 檢查Nest字段是否大於1,如果大於1則簡單的將Nest減1並繼續擁有鎖,如果等於1,則進入到步驟3。
  3. 檢查rfThis是否大於0,設置Owner爲NULL然後喚醒一個正在阻塞或等待的線程再一次試圖獲取鎖,如果等於0則進入到步驟4。
  4. 縮小(deflate)一個對象,通過將對象的LockWord置換回原來的HashCode等值來解除和monitor之間的關聯來釋放鎖,同時將monitor放回到線程私有的可用monitor列表。

重入鎖 & 非重入鎖
可重入鎖指同一個線程可以再次獲得之前已經獲得的鎖,避免產生死鎖。
當線程請求一個由其它線程持有的對象鎖時,該線程會阻塞,而當線程請求由自己持有的對象鎖時,如果該鎖是重入鎖,請求就會成功,否則阻塞。
下面爲可重入鎖與非可重入鎖的實現區別
不可重入鎖

public class Lock{
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException{
        while(isLocked){    
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}

可重入鎖
重入鎖的一種實現方法是爲每個鎖關聯一個線程持有者和計數器,當計數器爲0時表示該鎖沒有被任何線程持有,那麼任何線程都可能獲得該鎖而調用相應的方法;當某一線程請求成功後,JVM會記下鎖的持有線程,並且將計數器置爲1;此時其它線程請求該鎖,則必須等待;而如果同一個線程再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增;當線程退出同步代碼塊時,計數器會遞減,如果計數器爲0,則釋放該鎖。

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock()
            throws InterruptedException{
        Thread thread = Thread.currentThread();
        while(isLocked && lockedBy != thread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    public synchronized void unlock(){
        if(Thread.currentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

lockBy:保存已經獲得鎖實例的線程,在lock()判斷調用lock的線程是否已經獲得當前鎖實例,如果已經獲得鎖,則直接跳過while,無需等待。
lockCount:記錄同一個線程重複對一個鎖對象加鎖的次數。否則,一次unlock就會解除所有鎖,即使這個鎖實例已經加鎖多次了。
兩種鎖舉例

public class Count{
    Lock lock = new Lock();
    public void print(){
        lock.lock();
        doAdd();
        lock.unlock();
    }
    public void doAdd(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

對於不可重入鎖,當一個線程調用print()方法時,獲得了鎖,這時就無法再調用doAdd()方法,這時必須先釋放鎖才能調用,所以稱這種鎖爲不可重入鎖,也叫自旋鎖。
對於可重入鎖,可重入就意味着:線程可以進入任何一個它已經擁有的鎖所同步着的代碼塊。
第一個線程執行print()方法,得到了鎖,使lockedBy等於當前線程,也就是說,執行的這個方法的線程獲得了這個鎖,執行add()方法時,同樣要先獲得鎖,因不滿足while循環的條件,也就是不等待,繼續進行,將此時的lockedCount變量,也就是當前獲得鎖的數量加一,當釋放了所有的鎖,才執行notify()。如果在執行這個方法時,有第二個線程想要執行這個方法,因爲lockedBy不等於第二個線程,導致這個線程進入了循環,也就是等待,不斷執行wait()方法。只有當第一個線程釋放了所有的鎖(一共兩個鎖:print方法一個鎖+add方法一個鎖),執行了notify()方法,第二個線程才得以跳出循環,繼續執行。
java中常用的可重入鎖

  • synchronized
  • java.util.concurrent.locks.ReentrantLock

注意:
這裏要區別,同一個對象的多方法都加入synchronized關鍵字時,線程A 訪問 (synchronized)object.A,線程B 訪問 (synchronized)object.B時,必須等線程A訪問完A,線程B才能訪問B;此結論同樣適用於對於object中使用synchronized(this)同步代碼塊的場景;synchronized鎖定的都是當前對象!

lock 機制(單獨使用)實現線程競爭
在多線程環境下,synchronized塊中的方法獲取了lock實例的monitor,如果實例相同,那麼只有一個線程(通過競爭獲取到lock實例的線程)能執行該塊內容。

//通過lock鎖定
public class Thread1 implements Runnable {
        Object lock;
        public void run() {  
            synchronized(lock){
              ..do something
            }
        }
}
//直接用於方法
public class Thread1 implements Runnable {
        public synchronized void run() {  
             ..do something
        }
}

synchronized使用
(1)同步方法 synchronized關鍵字修飾的方法
由於java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處於阻塞狀態。
此時該內置鎖爲對象鎖/類鎖。我們知道,類的對象實例可以有很多個,但是每個類只有一個class對象,所以不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖。

public synchronized void save(){}

注: synchronized關鍵字也可以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個類
(1.1)對象鎖
對象鎖是用於對象實例方法,或者一個對象實例上的
(1.2)類鎖
類鎖是用於類的靜態方法或者一個類的class對象上的
對象鎖是用來控制實例方法之間的同步,類鎖是用來控制靜態方法(或靜態變量互斥體)之間的同步。其實類鎖只是一個概念上的東西,並不是真實存在的,它只是用來幫助我們理解鎖定實例方法和靜態方法的區別的。我們都知道,java類可能會有很多個對象,但是隻有1個Class對象,也就是說類的不同實例之間共享該類的Class對象。Class對象其實也僅僅是1個java對象,只不過有點特殊而已。由於每個java對象都有1個互斥鎖,而類的靜態方法是需要Class對象。所以所謂的類鎖,不過是Class對象的鎖而已。獲取類的Class對象有好幾種,最簡單的就是MyClass.class的方式。
(1.3)對象鎖與類鎖的區別
synchronized是對類的當前實例進行加鎖,防止其他線程同時訪問該類的該實例的所有synchronized塊,注意這裏是“類的當前實例”,類的兩個不同實例就沒有這種約束了。那麼static synchronized恰好就是要控制類的所有實例的訪問了,static synchronized是限制線程同時訪問jvm中該類的所有實例同時訪問對應的代碼快。實際上,在類中某方法或某代碼塊中有 synchronized,那麼在生成一個該類實例後,該類也就有一個監視快,放置線程併發訪問該實例synchronized保護快,而static synchronized則是所有該類的實例公用一個監視快了,也就是兩個的區別了,也就是synchronized相當於this.synchronized,而staticsynchronized相當於Something.synchronized.

      pulbic class Something(){
         public synchronized void isSyncA(){}
         public synchronized voidisSyncB(){}
         public static synchronizedvoid cSyncA(){}
         public static synchronizedvoid cSyncB(){}
     }

那麼,假如有Something類的兩個實例a與b,那麼下列組方法何以被1個以上線程同時訪問呢

a.   x.isSyncA()與x.isSyncB() 
b.   x.isSyncA()與y.isSyncA()
c.   x.cSyncA()與y.cSyncB()
d.   x.isSyncA()與Something.cSyncA()

a,都是對同一個實例的synchronized域訪問,因此不能被同時訪問
b,是針對不同實例的,因此可以同時被訪問
c,因爲是staticsynchronized,所以不同實例之間仍然會被限制,相當於Something.isSyncA()與   Something.isSyncB()了,因此不能被同時訪問。
d,是可以被同時訪問的,答案理由是synchronzied的是實例方法與synchronzied的類方法由於鎖定(lock)不同的原因。
個人分析也就是synchronized 與static synchronized 相當於兩幫派,各自管各自,相互之間就無約束了,可以被同時訪問。後面一部分將詳細分析synchronzied是怎麼樣實現的。

結論:

  1. synchronized static是某個類的範圍,synchronized static cSync{}防止多個線程同時訪問這個類中的synchronized static 方法。它可以對類的所有對象實例起作用。
  2. synchronized 是某實例的範圍,synchronized isSync(){}防止多個線程同時訪問這個實例中的synchronized 方法。
  3. 類鎖和對象鎖不是同1個東西,一個是類的Class對象的鎖,一個是類的實例的鎖。也就是說:1個線程訪問靜態synchronized的時候,允許另一個線程訪問對象的實例synchronized方法。反過來也是成立的,因爲他們需要的鎖是不同的。

(2)同步代碼塊 synchronized關鍵字修飾的語句塊
但用Synchronized修飾同步方法有缺陷:
當某個線程進入同步方法獲得對象鎖,那麼其他線程訪問這裏對象的同步方法時,必須等待或者阻塞,這對高併發的系統是致命的,這很容易導致系統的崩潰。如果某個線程在同步方法裏面發生了死循環,那麼它就永遠不會釋放這個對象鎖,那麼其他線程就要永遠的等待。這是一個致命的問題。
因此用synchronized修飾代碼塊,縮小同步範圍,減少了風險。
因此採用同步代碼塊,被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。
代碼如:

synchronized(object){ 
}

注:同步是一種高開銷的操作,因此應該儘量減少同步的內容。
通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。

public class TestSynchronized   
{    
    public void test1()   
    {    
         synchronized(this)   
         {    
              int i = 5;    
              while( i-- > 0)   
              {    
                   System.out.println(Thread.currentThread().getName() + " : " + i);    
                   try   
                   {    
                        Thread.sleep(500);    
                   }   
                   catch (InterruptedException ie)   
                   {    
                   }    
              }    
         }    
    }    
      
    public synchronized void test2()   
    {    
         int i = 5;    
         while( i-- > 0)   
         {    
              System.out.println(Thread.currentThread().getName() + " : " + i);    
              try   
              {    
                   Thread.sleep(500);    
              }   
              catch (InterruptedException ie)   
              {    
              }    
         }    
    }    
      
    public static void main(String[] args)   
    {    
         final TestSynchronized myt2 = new TestSynchronized();    
         Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );    
         Thread test2 = new Thread(  new Runnable() {  public void run() { myt2.test2();   }  }, "test2"  );    
         test1.start();;    
         test2.start();    
//         TestRunnable tr=new TestRunnable();  
//         Thread test3=new Thread(tr);  
//         test3.start();  
    }   
    
}  

執行結果

test2 : 4  
test2 : 3  
test2 : 2  
test2 : 1  
test2 : 0  
test1 : 4  
test1 : 3  
test1 : 2  
test1 : 1  
test1 : 0  

上述的代碼,第一個方法時用了同步代碼塊的方式進行同步,傳入的對象實例是this,表明是當前對象,當然,如果需要同步其他對象實例,也不可傳入其他對象的實例;第二個方法是修飾方法的方式進行同步。因爲第一個同步代碼塊傳入的this,所以兩個同步代碼所需要獲得的對象鎖都是同一個對象鎖,下面main方法時分別開啓兩個線程,分別調用test1和test2方法,那麼兩個線程都需要獲得該對象鎖,另一個線程必須等待。上面也給出了運行的結果可以看到:直到test2線程執行完畢,釋放掉鎖,test1線程纔開始執行。(可能這個結果有人會有疑問,代碼裏面明明是先開啓test1線程,爲什麼先執行的是test2呢?這是因爲java編譯器在編譯成字節碼的時候,會對代碼進行一個重排序,也就是說,編譯器會根據實際情況對代碼進行一個合理的排序,編譯前代碼寫在前面,在編譯後的字節碼不一定排在前面,所以這種運行結果是正常的, 這裏是題外話,最主要是檢驗synchronized的用法的正確性)
synchronized同時修飾靜態方法和實例方法,但是運行結果是交替進行的,這證明了類鎖和對象鎖是兩個不一樣的鎖,控制着不同的區域,它們是互不干擾的。同樣,線程獲得對象鎖的同時,也可以獲得該類鎖,即同時獲得兩個鎖,這是允許的。
一個類的對象鎖和另一個類的對象鎖是沒有關聯的,當一個線程獲得A類的對象鎖時,它同時也可以獲得B類的對象鎖。

wait/notify 機制實現線程協作
wait/notify機制:在Java中,可以通過配合調用Object對象的wait()方法和notify()方法或notifyAll()方法來實現線程間的通信。
由於 wait()、notify/notifyAll() 在synchronized 代碼塊執行,說明當前線程一定是獲取了鎖的。
當線程執行wait()方法時候,會將當前進程阻塞,釋放當前的鎖,然後讓出CPU,進入等待狀態。(直到接到通知或被中斷爲止)
只有當 notify/notifyAll() 被執行時候,纔會喚醒一個或多個正處於等待狀態的線程,從wait()方法中繼續往下執行。
要注意

  1. notify喚醒阻塞的線程後,線程會接着上次的執行繼續往下執行。
  2. wait/notify必須在同步方法或同步快中調用。wait()方法釋放當前線程的鎖,因此如果當前線程沒有持有適當的鎖,則拋出IllegalMonitorStateException異常。notify()方法調用前,線程也必須要獲得該對象的對象級別鎖,的如果調用notify()時沒有持有適當的鎖,也會拋出IllegalMonitorStateException。
  3. notify與notifyall區別與聯繫
    notify 與 notifyall 都是用於喚醒被 wait 的線程
    notify 調用後,如果有多個線程等待,則線程規劃器任意挑選出其中一個wait()狀態的線程來發出通知,並使它等待獲取該對象的對象鎖。但不驚動其他同樣在等待被該對象notify的線程們。當第一個獲得了該對象鎖的wait線程運行完畢以後,它會釋放掉該對象鎖,此時如果該對象沒有再次使用notify語句,則即便該對象已經空閒,其他wait狀態等待的線程由於沒有得到該對象的通知,會繼續阻塞在wait狀態,直到這個對象發出一個notify或notifyAll。
    notifyAll使所有原來在該對象上wait的線程統統退出wait的狀態(即全部被喚醒,不再等待notify或notifyAll,但由於此時還沒有獲取到該對象鎖,因此還不能繼續往下執行),變成等待獲取該對象上的鎖,一旦該對象鎖被釋放(notifyAll線程退出調用了notifyAll的synchronized代碼塊的時候),他們就會去競爭。如果其中一個線程獲得了該對象鎖,它就會繼續往下執行,在它退出synchronized代碼塊,釋放鎖後,其他的已經被喚醒的線程將會繼續競爭獲取該鎖,一直進行下去,直到所有被喚醒的線程都執行完畢。
  4. notify後,當前線程不會馬上釋放該對象鎖,wait所在的線程並不能馬上獲取該對象鎖,要等到程序退出synchronized代碼塊後,當前線程纔會釋放鎖,wait所在的線程也纔可以通過競爭獲取該對象鎖

典型案例:生產者-消費者問題
兩個進程共享一個公共的固定大小的緩衝區。其中一個是生產者,用於將消息放入緩衝區;另外一個是消費者,用於從緩衝區中取出消息。問題出現在當緩衝區已經滿了,而此時生產者還想向其中放入一個新的數據項的情形,其解決方法是讓生產者此時進行休眠,等待消費者從緩衝區中取走了一個或者多個數據後再去喚醒它。同樣地,當緩衝區已經空了,而消費者還想去取消息,此時也可以讓消費者進行休眠,等待生產者放入一個或者多個數據時再喚醒它。

	/**
     * 生產者生產出來的產品交給店員
     */
    public synchronized void produce()
    {
        if(this.product >= MAX_PRODUCT)
        {
            try
            {
                wait();  
                System.out.println("產品已滿,請稍候再生產");
            }
            catch(InterruptedException e)
            {
                e.printStackTrace();
            }
            return;
        }
        
        this.product++;
        System.out.println("生產者生產第" + this.product + "個產品.");
        notifyAll();   //通知等待區的消費者可以取出產品了
    }
    
    /**
     * 消費者從店員取產品
     */
    public synchronized void consume()
    {
        if(this.product <= MIN_PRODUCT)
        {
            try 
            {
                wait(); 
                System.out.println("缺貨,稍候再取");
            } 
            catch (InterruptedException e) 
            {
                e.printStackTrace();
            }
            return;
        }
        
        System.out.println("消費者取走了第" + this.product + "個產品.");
        this.product--;
        notifyAll();   //通知等待去的生產者可以生產產品了
    }

(1.2.2)Volatile

1.Java多線程內存模式 與 重排序
在JAVA多線程環境下,對於每個Java線程除了共享的虛擬機棧外和Java堆之外,還存在一個獨立私有的工作內存,工作內存存放主存中變量的值的拷貝。每個線程獨立運行,彼此之間都不可見,線程的私有堆內存中保留了一份主內存的拷貝,只有在特定需求的情況下才會與主存做交互(複製/刷新)。
當數據從主內存複製到工作存儲時,必須出現兩個動作:第一,由主內存執行的讀(read)操作;第二,由工作內存執行的相應的load操作;當數據從工作內存拷貝到主內存時,也出現兩個操作:第一個,由工作內存執行的存儲(store)操作;第二,由主內存執行的相應的寫(write)操作
每一個操作都是原子的,即執行期間不會被中斷。
對於普通變量,一個線程中更新的值,不能馬上反應在其他變量中。
如果需要在其他線程中立即可見,需要使用 volatile 關鍵字。
在這裏插入圖片描述
在這裏插入圖片描述
在有些場景下多線程訪問程序變量會表現出與程序制定的順序不一樣。因爲編譯器可以以優化的名義改變每個獨立線程的順序,從而使處理器不按原來的順序執行線程。一個Java程序在從源代碼到最終實際執行的指令序列之間,會經歷一系列的重排序過程。
對於多線程共享同一內存區域這一情況,使得每個線程不知道其他線程對數據做了怎樣的修改(數據修改位於線程的私有內存中,具有不可見性),從而導致執行結果不正確。因此必須要解決這一同步問題。
2.volatile原理
對於非volatile變量進行讀寫時,每個寫成先從主存拷貝變量到線程緩存中,執行完操作再保存到主存中。需要進行load/save操作。
而volatile變量保證每次讀寫變量都是不經過緩存而是直接從內存讀寫數據。省去了load/save操作。volatile變量不會將對該變量的操作與其他內存操作一起重排序,能及時更新到主存;且因該變量存儲在主存上,所以總會返回最新寫入的值。

因此volatile定義的變量具有以下特性:

  1. 保證此變量對所有的線程的可見性。
    當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內存,以及每次使用前立即從主內存更新。因此使用volatile修飾域相當於告訴JVM該域會被其他線程更新,volatile修飾域一旦改變,相當於告訴所有其他線程該域的變化。但非volatile變量的值在線程間傳遞均需要通過主內存完成,看到的數據可能不是最新的數據。
  2. 禁止指令重排序優化。
    有volatile修飾的變量,賦值後多執行了一個“load and save”操作,這個操作相當於一個內存屏障(指令重排序時不能把後面的指令重排序到內存屏障之前的位置)
  3. 性能較低
    volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因爲它需要在本地代碼中插入許多內存屏障指令來保證處理器不亂序執行。
  4. 輕量級sychronized
    在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。

3.volatile實例

class Bank {
            //需要同步的變量加上volatile
            private volatile int account = 100;

            public int getAccount() {
                return account;
            }
            //這裏不再需要synchronized 
            public void save(int money) {
                account += money;
            }
        }

4. Volatile、Synchronized 與 Final 對比
在這裏插入圖片描述

  1. final不可變
    作用於類、方法、成員變量、局部變量。初始化完成後的不可變對象,其它線程可見。常量不會改變不會因爲其它線程產生影響。Final修飾的引用類型的地址不變,同時需要保證引用類型各個成員和操作的線程安全問題。因爲引用類型成員可能是可變的。
  2. synchronized同步
    作用域代碼塊、方法上。通過線程互斥,同一時間的同樣操作只允許一個線程操作。通過字節碼指令實現。
  3. Volatile 修飾域
  • volatile 修飾的變量的變化保證對其它線程立即可見。
    volatile變量的寫,先發生於讀。每次使用volatile修飾的變量個線程都會刷新保證變量一致性。但同步之前各線程可能仍有操作。如:各個根據volatile變量初始值分別進行一些列操作,然後再同步寫賦值。每個線程的操作有先後,當一個最早的線程給線程賦值時,其它線程同步。但這時其它線程可能根據初始值做了改變,同步的結果導致其它線程工作結果丟失。根據volatile的語意使用條件:運算結果不依賴變量的當前值。
  • volatile禁止指令重排優化。
    這個語意導致寫操作會慢一些。因爲讀操作跟這個沒關係。

(1.2.3)ReentrantLock

在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。
lock: 在java.util.concurrent包內。共有三個實現:
//ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,它與使用synchronized方法和快具有相同的基本行爲和語義,並且擴展了其能力。

  • ReentrantLock
  • ReentrantReadWriteLock.ReadLock
  • ReentrantReadWriteLock.WriteLock

主要目的是和synchronized一樣, 兩者都是爲了解決同步問題,處理資源爭端而產生的技術。功能類似但有一些區別。
區別如下:

  • lock更靈活,可以自由定義多把鎖的枷鎖解鎖順序(synchronized要按照先加的後解順序)
  • 提供多種加鎖方案,lock 阻塞式, trylock 無阻塞式, lockInterruptily 可打斷式, 還有trylock的帶超時時間版本。
  • 本質上和監視器鎖(即synchronized是一樣的)
  • 能力越大,責任越大,必須控制好加鎖和解鎖,否則會導致災難。
  • 和Condition類的結合。
  • 性能更高

ReenreantLock類實現原理
輕鬆學習java可重入鎖(ReentrantLock)的實現原理
ReentrantLock支持兩種獲取鎖的方式,一種是公平模型,一種是非公平模型。
ReentrantLock結構如下

volatile int state 表示臨界資源佔有狀態
Thread 正在執行的線程
Node 雙向隊列 處於等待阻塞的節點
  • 公平鎖模型
  1. 初始化時,state=0,表示沒有線程佔用資源。線程A請求鎖。
  2. 線程A 獲得鎖,state原子性+1 並執行任務。線程B請求鎖。
  3. 線程B無法獲得鎖,生成節點進行排隊(Node隊列)。線程A再次請求鎖。
  4. 此時的線程A不需要排隊,直接得到鎖,執行任務,state原子性+1(可重入鎖:一個線程在獲取了鎖之後,再次去獲取了同一個鎖,這時候僅僅是把狀態值進行累加)。
  5. 線程A釋放了一次鎖,則state原子性 -1,只有當線程A 將鎖全部釋放,state=0時,其他線程纔有機會獲取鎖,此時會通知隊列喚醒 線程B節點,使線程 B 可以參與競爭。
  6. 若線程B競爭獲得鎖,則對應結點從隊列中刪除。
  • 不公平鎖模型
    當線程A執行完之後,要喚醒線程B是需要時間的,而且線程B醒來後還要再次競爭鎖,所以如果在切換過程當中,來了一個線程C,那麼線程C是有可能獲取到鎖的,如果C獲取到了鎖,B就只能繼續乖乖休眠了。

即公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序(隊列的先後順序)來依次獲得鎖。而不公平鎖則不用按照申請鎖的時間順序獲得鎖。synchronized 中的鎖是非公平的,ReentrantLock 默認情況下也是非公平的,但是也可以是公平的。

ReenreantLock類的常用方法

  • ReentrantLock() : 創建一個ReentrantLock實例
  • lock() : 獲得鎖
  • unlock() : 釋放鎖
class Bank {

            private int account = 100;
            //需要聲明這個鎖
            private Lock lock = new ReentrantLock();
            public int getAccount() {
                return account;
            }
            //這裏不再需要synchronized 
            public void save(int money) {
                lock.lock();
                try{
                    account += money;
                }finally{
                    lock.unlock();
                }
            }
        }

ReenreantLock & Synchronized 的選擇

比較類型 Synchronized ReenreantLock
鎖的實現 JVM JDK
等待可中斷:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改爲處理其他事情 不可中斷 可中斷
公平鎖:公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。 非公平 公平/非公平(默認)

除非需要使用 ReentrantLock 的高級功能,否則優先使用 synchronized。這是因爲 synchronized 是 JVM 實現的一種鎖機制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。並且使用 synchronized 不用擔心沒有釋放鎖而導致死鎖問題,因爲 JVM 會確保鎖的釋放。

總結:Synchronized & ReentrantLock & Volatile 區別
(1)Synchronized &Volatile 區別
AbstractQueuedSynchronizer通過構造一個基於阻塞的CLH隊列容納所有的阻塞線程,而對該隊列的操作均通過Lock-Free(CAS)操作,但對已經獲得鎖的線程而言,ReentrantLock實現了偏向鎖的功能。
synchronized的底層也是一個基於CAS操作的等待隊列,但JVM實現的更精細,把等待隊列分爲ContentionList和EntryList,目的是爲了降低線程的出列速度;當然也實現了偏向鎖,從數據結構來說二者設計沒有本質區別。但synchronized還實現了自旋鎖,並針對不同的系統和硬件體系進行了優化,而Lock則完全依靠系統阻塞掛起等待線程。
當然Lock比synchronized更適合在應用層擴展,可以繼承AbstractQueuedSynchronizer定義各種實現,比如實現讀寫鎖(ReadWriteLock),公平或不公平鎖;同時,Lock對應的Condition也比wait/notify要方便的多、靈活的多。

(2)Synchronized & ReentrantLock 區別
volatile本質是在告訴jvm當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取; synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。
volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的
volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性
volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。
volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化

1.3)基本線程類

(1.3.1)Thread 類

Thread類實現了Runnable接口,在Thread類中,有一些比較關鍵的屬性

public class Thread implements Runnable{
	private char name[];//表示Thread名字,可以通過Thread構造器中的參數指定線程的名字
	private int priority;//線程的優先級(最大值爲10,最小值爲1,默認爲5)
	// 守護線程和用戶線程的區別在於:守護線程依賴於創建它的線程,而用戶線程則不依賴。舉個簡單的例子:如果在main線程中創建了一個守護線程,當main方法運行完畢之後,守護線程也會隨着消亡。而用戶線程則不會,用戶線程會一直運行直到其運行完畢。在JVM中,像垃圾收集器線程就是守護線程。
	private boolean daemon = false;//該線程是否爲守護線程
	private Runnable target;//要執行的任務
}

Thread類常用的方法如下

// start() 用來啓動一個線程,實現多線程,當調用start方法後,系統會開啓一個新線程用來執行用戶定義的子任務,併爲響應線程分配資源。這時線程處於就緒狀態,但並沒有運行,一旦得到cpu時間片,就開始執行run方法(run()稱爲線程體,包含要執行這個線程的內容,run()方法運行結束則線程終止)
public static Thread.start()
// run()方法是不需要用戶來調用的,當通過start方法啓動一個線程之後,當線程獲得了CPU執行時間,便進入run方法體去執行具體的任務。注意,繼承Thread類必須重寫run方法,在run方法中定義具體要執行的任務。
public static Thread.run()
// 當前線程可轉讓cpu控制權,讓別的就緒狀態線程運行(切換)
// 調用yield方法會讓當前線程交出CPU權限,讓CPU去執行其他的線程。它跟sleep方法類似,同樣不會釋放鎖。但是yield不能控制具體的交出CPU的時間,另外,yield方法只能讓擁有相同優先級的線程有獲取CPU執行時間的機會。
// 注意,調用yield方法並不會讓線程進入阻塞狀態,而是讓線程重回就緒狀態,它只需要等待重新獲取CPU執行時間,這一點是和sleep方法不一樣的。
public static Thread.yield() 
// sleep相當於讓線程睡眠,交出CPU,讓CPU去執行其他的任務。
// 但是有一點要非常注意,sleep方法不會釋放鎖(相當於一直持有該對象的鎖),也就是說如果當前線程持有對某個對象的鎖,則即使調用sleep方法,其他線程也無法訪問這個對象。
// 還有一點要注意,如果調用了sleep方法,必須捕獲InterruptedException異常或者將該異常向上層拋出。當線程睡眠時間滿後,不一定會立即得到執行,因爲此時可能CPU正在執行其他的任務。所以說調用sleep方法相當於讓線程進入阻塞狀態。
sleep(long millis)     //參數爲毫秒
sleep(long millis,int nanoseconds)    //第一參數爲毫秒,第二個參數爲納秒
// 在一個線程中調用other.join(),將等待other執行完後才繼續本線程。  
// 假如在main線程中,調用thread.join方法,則main方法會等待thread線程執行完畢或者等待一定的時間。如果調用的是無參join方法,則等待thread執行完畢,如果調用的是指定了時間參數的join方法,則等待一定的時間。  
join()
join(long millis)     //參數爲毫秒
join(long millis,int nanoseconds)    //第一參數爲毫秒,第二個參數爲納秒
// interrupt()是Thread類的一個實例方法,用於中斷本線程。這個方法被調用時,會立即將線程的中斷標誌設置爲“true”。所以當中斷處於“阻塞狀態”的線程時,由於處於阻塞狀態,中斷標記會被設置爲“false”,拋出一個 InterruptedException。所以我們在線程的循環外捕獲這個異常,就可以退出線程了。
// interrupt()並不會中斷處於“運行狀態”的線程,它會把線程的“中斷標記”設置爲true,所以我們可以不斷通過isInterrupted()來檢測中斷標記,從而在調用了interrupt()後終止線程,這也是通常我們對interrupt()的用法。
public interrupte()

Java中斷機制

  1. Java 中斷機制介紹
    java中的線程中斷機制是一種協作機制。線程會不時地檢測中斷標識位,以判斷線程是否應該被中斷(中斷標識值是否爲true)。
    Java提供了中斷機制,Threaf類下有3個重要的方法
  • public void interrupt();//每個線程都有個boolean類型的中斷狀態。當使用Thread的interrupt()方法時,線程的中斷狀態會被設置爲true。
  • public boolean isInterrupted();//判斷線程是否被中斷
  • public static boolean interrupted(); // 清除中斷標誌,並返回原狀態
    當一個線程中斷另一個線程時,被中斷的線程不一定要立即停止正在做的事情。相反,中斷是禮貌地請求另一個線程在它願意並且方便的時候停止它正在做的事情。interrupt方法,就是告訴線程,我需要中斷你,該方法調用之後,線程並不會立刻終止,而是在合適的時機終止。什麼時機呢?Java的處理判定機制爲:
    機制一:如果該線程處在可中斷狀態下,(例如Thread.sleep(), Thread.join()或 Object.wait()),那麼該線程會立即被喚醒,同時會收到一個InterruptedException,如果是阻塞在io上,對應的資源會被關閉。
    機制二:如果該線程處在不可中斷狀態下,即沒有調用上述api,處於運行時的進程。那麼java只是設置一下該線程的interrupt狀態,其他事情都不會發生,如果該線程之後會調用行數阻塞API,那到時候線程會馬會上跳出,並拋出InterruptedException,接下來的事情就跟第一種狀況一致了。如果不會調用阻塞API,那麼這個線程就會一直執行下去。在被中斷線程中運行的代碼以後可以輪詢中斷狀態,看看它是否被請求停止正在做的事情。中斷狀態可以通過 Thread.isInterrupted()來讀取,並且可以通過一個名爲Thread.interrupted() 的操作讀取和清除。
  1. 利用 中斷機制 正確結束線程
    綜合線程處於“阻塞狀態”和“運行狀態”的終止方式,比較通用的終止線程的形式如下:
public class InterruptedExample {

    public static void main(String[] args) throws Exception {
        InterruptedExample interruptedExample = new InterruptedExample();
        interruptedExample.start();
    }

    public void start() {
        MyThread myThread = new MyThread();
        myThread.start();

        try {
        //當Thread 處於 sleep 後處於阻塞狀態,收到中斷請求會跑出InterruptedException異常
            Thread.sleep(3000);
            myThread.cancel();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private class MyThread extends Thread{

        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                // 線程循環執行打印一些信息,使用isInterrupted判斷線程是否被中斷,若中斷則結束線程
                    System.out.println("test");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                	// 阻塞狀態下的線程拋出異常後則會被終止
                    System.out.println("interrupt");
                    // 拋出InterruptedException後中斷標誌被清除(中斷標誌 重新設置爲false)
                    // 標準做法是再次調用interrupt恢復中斷,正確情景下爲true
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("stop");
        }

        public void cancel(){
        //對線程調用interrupt()方法,不會真正中斷正在運行的線程,
        //只是發出一個請求,由線程在合適時候結束自己。
            interrupt();
        }
    }
}

線程執行方法與狀態的聯繫
在這裏插入圖片描述
Thread類實現案例
(1)繼承Thread類,重寫該類的run()方法,run方法的方法體代表了線程要完成的任務,因此run方法可稱爲執行體
(2)創建Thread子類的實例,即創建線程對象
(3)調用線程對象的start方法啓動線程

package com.thread;
 
public class FirstThreadTest extends Thread{
	int i = 0;
	//重寫run方法,run方法的方法體就是現場執行體
	public void run()
	{
		for(;i<100;i++){
		System.out.println(getName()+"  "+i);
		
		}
	}
	public static void main(String[] args)
	{
		for(int i = 0;i< 100;i++)
		{
			// 調用100次main主線程
			System.out.println(Thread.currentThread().getName()+"  : "+i);
			if(i==20)
			{
				// 當主線程調用到20時,執行100次子線程-1,子線程-2
				new FirstThreadTest().start();
				new FirstThreadTest().start();
			}
		}
	}
 
}

實現結果

main  : 18
main  : 19
main  : 20
Thread-0  0
Thread-0  1
main  : 21
Thread-0  2
Thread-1  0
main  : 22
main  : 23
Thread-1  1
Thread-0  3

(1.3.2)Runnable 接口

(1)定義runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
(2)創建 Runnable實現類的實例,並依此實例作爲Thread的target來創建Thread對象,該Thread對象纔是真正的線程對象。
(3)調用線程對象的start()方法來啓動該線程。


package com.thread;
 
public class RunnableThreadTest implements Runnable
{
 
	private int i;
	public void run()
	{
		for(i = 0;i <100;i++)
		{
			System.out.println(Thread.currentThread().getName()+" "+i);
		}
	}
	public static void main(String[] args)
	{
		for(int i = 0;i < 100;i++)
		{
			System.out.println(Thread.currentThread().getName()+" "+i);
			if(i==20)
			{
				RunnableThreadTest rtt = new RunnableThreadTest();
				new Thread(rtt,"新線程1").start();
				new Thread(rtt,"新線程2").start();
			}
		}
 
	}
 
}

(1.3.3)Callable 接口

通過Callable和FutureTask創建線程
(1)創建Callable接口的實現類,並實現call()方法,該call()方法將作爲線程執行體,並且有返回值。
(2)創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。
(3)使用FutureTask對象作爲Thread對象的target創建並啓動新線程。
(4)調用FutureTask對象的get()方法來獲得子線程執行結束後的返回值

package com.thread;
 
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class CallableThreadTest implements Callable<Integer>
{
 
	public static void main(String[] args)
	{
		// 創建Callable實現體的實例,使用FutureTask類包裝Callable對象
		CallableThreadTest ctt = new CallableThreadTest();
		FutureTask<Integer> ft = new FutureTask<>(ctt);
		for(int i = 0;i < 100;i++)
		{
			System.out.println(Thread.currentThread().getName()+" 的循環變量i的值"+i);
			if(i==20)
			{
			//使用FutureTask對象作爲Thread對象的target創建並啓動新線程
				new Thread(ft,"有返回值的線程").start();
			}
		}
		try
		{
		//調用FutureTask對象的get()方法來獲得子線程執行結束後的返回值
			System.out.println("子線程的返回值:"+ft.get());
		} catch (InterruptedException e)
		{
			e.printStackTrace();
		} catch (ExecutionException e)
		{
			e.printStackTrace();
		}
 
	}
 
	@Override
	public Integer call() throws Exception
	{
	// call 方法即爲線程的執行體,並且擁有返回值
		int i = 0;
		for(;i<100;i++)
		{
			System.out.println(Thread.currentThread().getName()+" "+i);
		}
		return i;
	}
 
}

創建線程的三種方式及對比

創建方式 使用方式 優勢 劣勢
Thread 繼承Thread類創建線程類,並重寫run方法 編寫簡單,如果需要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前線程。 線程類已經繼承了Thread類,所以不能再繼承其他父類。
Runnable接口 實現Runnable接口創建線程類,並實現run方法 線程類只是實現了Runnable接口或Callable接口,還可以繼承其他類。在這種方式下,多個線程可以共享同一個target對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU、代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。 編程稍微複雜,如果要訪問當前線程,則必須使用Thread.currentThread()方法。
Callable接口 實現Callable接口創建線程類,並用FutureTask類包裝Callable對象,並實現call方法 同上 同上

1.4)高級多線程控制類

Java1.5提供了一個非常高效實用的多線程包:java.util.concurrent, 提供了大量高級工具,可以幫助開發者編寫高效、易維護、結構清晰的Java多線程程序。

(1.4.1)ThreadLocal類

用處:保存線程的獨立變量。對一個線程類(繼承自Thread)
當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,副本之間相互獨立,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。常用於用戶登錄控制,如記錄session信息。
實現:每個Thread都持有一個TreadLocalMap類型的變量(該類是一個輕量級的Map,功能與map一樣,區別是桶裏放的是entry而不是entry的鏈表。功能還是一個map。)以本身爲key,以目標爲value。
主要方法是get()和set(T a),set之後在map裏維護一個threadLocal -> a,get時將a返回。ThreadLocal是一個特殊的容器。
ThreadLocal 類的常用方法

  • ThreadLocal() : 創建一個線程本地變量
  • get() : 返回此線程局部變量的當前線程副本中的值
  • initialValue() : 返回此線程局部變量的當前線程的"初始值"
  • set(T value) : 將此線程局部變量的當前線程副本中的值設置爲value
public class Bank{
            //使用ThreadLocal類管理共享變量account
            private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){
                @Override
                protected Integer initialValue(){
                    return 100;
                }
            };
            public void save(int money){
                account.set(account.get()+money);
            }
            public int getAccount(){
                return account.get();
            }
        }

(1.4.2)原子類(AtomicInteger、AtomicBoolean……)

如果使用atomic wrapper class如atomicInteger,或者使用自己保證原子的操作,則等同於synchronized

//返回值爲boolean
AtomicInteger.compareAndSet(int expect,int update)

該方法可用於實現樂觀鎖,考慮文中最初提到的如下場景:a給b付款10元,a扣了10元,b要加10元。此時c給b2元,但是b的加十元代碼約爲:

if(b.value.compareAndSet(old, value)){
   return ;
}else{
   //try again
   // if that fails, rollback and log
}

AtomicReference
對於AtomicReference 來講,也許對象會出現,屬性丟失的情況,即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。
這時候,AtomicStampedReference就派上用場了。這也是一個很常用的思路,即加上版本號

(1.4.3)容器類

  • BlockingQueue
    阻塞隊列。該類是java.util.concurrent包下的重要類,通過對Queue的學習可以得知,這個queue是單向隊列,可以在隊列頭添加元素和在隊尾刪除或取出元素。類似於一個管  道,特別適用於先進先出策略的一些應用場景。普通的queue接口主要實現有PriorityQueue(優先隊列)。
    除了傳統的queue功能(表格左邊的兩列)之外,還提供了阻塞接口put和take,帶超時功能的阻塞接口offer和poll。put會在隊列滿的時候阻塞,直到有空間時被喚醒;take在隊 列空的時候阻塞,直到有東西拿的時候才被喚醒。用於生產者-消費者模型尤其好用,堪稱神器。
  • ConcurrentHashMap
    高效的線程安全哈希map。請對比hashTable , concurrentHashMap, HashMap

(1.4.4)Semaphore —— 控制併發線程數

簡介
信號量(Semaphore),有時被稱爲信號燈,是在多線程環境下使用的一種設施, 它負責協調各個線程, 以保證它們能夠正確、合理的使用公共資源。Semaphore分爲單值和多值兩種,前者只能被一個線程獲得,後者可以被若干個線程獲得。
信號量的特性如下:
在多線程對一個(多個)公共資源進行訪問的場景下,
信號量是一個非負整數(表示可以併發訪問公共資源的線程數),所有通過它的線程都會將該整數減一(可使用的公共資源數目-1),當該整數值爲零時,所有試圖通過它的線程都將處於等待狀態。在信號量上我們定義兩種操作: Wait(等待) 和 Release(釋放)。 當一個線程調用Wait(等待)操作時,它要麼通過然後將信號量減一(Semaphore>0);要麼一直等下去(Semaphore<=0),直到信號量大於0或超時。Release(釋放)實際上是在信號量上執行加操作,該操作之所以叫做“釋放”是因爲加操作實際上是釋放了由信號量守護的公共資源。
在java中,還可以設置該信號量是否採用公平模式,如果以公平方式執行,則線程將會按到達的順序(FIFO)執行,如果是非公平,則可以後請求的有可能排在隊列的頭部。
Java 使用

Semaphore(int permits, boolean fair)
//創建具有給定的許可數和給定的公平設置的Semaphore。

Semaphore當前在多線程環境下被擴放使用,操作系統的信號量是個很重要的概念,在進程控制方面都有應用。Java併發庫Semaphore 可以很輕鬆完成信號量控制,Semaphore可以控制某個資源可被同時訪問的個數,通過 acquire() 獲取一個許可,如果沒有就等待,而 release() 釋放一個許可。比如在Windows下可以設置共享文件的最大客戶端訪問個數。
Semaphore實現的功能就類似廁所有5個坑,假如有10個人要上廁所,那麼同時只能有多少個人去上廁所呢?同時只能有5個人能夠佔用,當5個人中 的任何一個人讓開後,其中等待的另外5個人中又有一個人可以佔用了。另外等待的5個人中可以是隨機獲得優先機會,也可以是按照先來後到的順序獲得機會,這取決於構造Semaphore對象時傳入的參數選項。單個信號量的Semaphore對象可以實現互斥鎖的功能,並且可以是由一個線程獲得了“鎖”,再由另一個線程釋放“鎖”,這可應用於死鎖恢復的一些場合。
實現案例
模擬上述實例,創建一個 同一時間最多隻有5個線程訪問 的連接池。

package SemaPhore;
 
import java.util.Random;
import java.util.concurrent.*;
public class Test {
 
 
 
	public static void main(String[] args) {
		//線程池
		ExecutorService executor = Executors.newCachedThreadPool();
		//定義信號量,只能5個線程同時訪問
        final Semaphore semaphore = new Semaphore(5);
        //模擬20個線程同時訪問
        for (int i = 0; i < 20; i++) {
        	 final int NO = i;
			 Runnable runnable = new Runnable() {
				public void run() {
					try {
						//獲取許可
						semaphore.acquire();
						//availablePermits()指的是當前信號燈庫中有多少個可以被使用
						System.out.println("線程" + Thread.currentThread().getName() +"進入,當前已有" + (5-semaphore.availablePermits()) + "個併發");
					    System.out.println("index:"+NO);
						Thread.sleep(new Random().nextInt(1000)*10);
					    
						System.out.println("線程" + Thread.currentThread().getName() + "即將離開");	
					    //訪問完後,釋放
					    semaphore.release();
 
					
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			};
			
			executor.execute(runnable);
        }
        // 退出線程池
        executor.shutdown();
	}
 
}

(1.4.5)Java 線程池

  1. 爲什麼用線程池
  • 創建/銷燬線程伴隨着系統開銷,過於頻繁的創建/銷燬線程,會很大程度上影響處理效率

例如:
記創建線程消耗時間T1,執行任務消耗時間T2,銷燬線程消耗時間T3
如果T1+T3>T2,那麼是不是說開啓一個線程來執行這個任務太不划算了!
正好,線程池緩存線程,可用已有的閒置線程來執行新任務,避免了T1+T3帶來的系統開銷

  • 線程併發數量過多,搶佔系統資源從而導致阻塞

我們知道線程能共享系統資源,如果同時執行的線程過多,就有可能導致系統資源不足而產生阻塞的情況。運用線程池能有效的控制線程最大併發數,避免以上的問題

  • 對線程進行一些簡單的管理

比如:延時執行、定時循環執行的策略等 運用線程池都能進行很好的實現

  1. 線程池簡介
    在Java中,線程池的概念是Executor這個接口,具體實現爲ThreadPoolExecutor類。

Executor接口是Executor框架的一個最基本的接口,Executor框架的大部分類都直接或間接地實現了此接口。 只有一個方法
void execute(Runnable command):
在未來某個時間執行給定的命令。該命令可能在新的線程、已入池的線程或者正調用的線程中執行,這由 Executor 實現決定。

  1. 線程池使用策略
    (3.1)構造
  • int corePoolSize,線程池中核心線程數最大值
    線程池新建線程的時候,如果當前線程總數小於corePoolSize,則新建的是核心線程,如果超過corePoolSize,則新建的是非核心線程
    核心線程默認情況下會一直存活在線程池中,即使這個核心線程啥也不幹(閒置狀態)。
    如果指定ThreadPoolExecutor的allowCoreThreadTimeOut這個屬性爲true,那麼核心線程如果不幹活(閒置狀態)的話,超過一定時間(時長下面參數決定),就會被銷燬掉
  • int maximumPoolSize,線程池中線程總數最大值
    線程總數 = 核心線程數 + 非核心線程數。
  • long keepAliveTime,線程池中非核心線程閒置超時時長
    一個非核心線程,如果不幹活(閒置狀態)的時長超過這個參數所設定的時長,就會被銷燬掉
    如果設置allowCoreThreadTimeOut = true,則會作用於核心線程
  • TimeUnit unit,枚舉類型,keepAliveTime的單位
  • BlockingQueue workQueue,線程池中任務隊列:維護着等待執行的Runnable對象
    當所有的核心線程都在幹活時,新添加的任務會被添加到這個隊列中等待處理,如果隊列滿了,則新建非核心線程執行任務。
    常用的workQueue類型:
    SynchronousQueue
    這個隊列接收到任務的時候,會直接提交給線程處理,而不保留它,如果所有線程都在工作怎麼辦?那就新建一個線程來處理這個任務!所以爲了保證不出現<線程數達到了maximumPoolSize而不能新建線程>的錯誤,使用這個類型隊列的時候,maximumPoolSize一般指定成Integer.MAX_VALUE,即無限大
    LinkedBlockingQueue
    這個隊列接收到任務的時候,如果當前線程數小於核心線程數,則新建線程(核心線程)處理任務;如果當前線程數等於核心線程數,則進入隊列等待。由於這個隊列沒有最大值限制,即所有超過核心線程數的任務都將被添加到隊列中,這也就導致了maximumPoolSize的設定失效,因爲總線程數永遠不會超過corePoolSize
    ArrayBlockingQueue
    可以限定隊列的長度,接收到任務的時候,如果沒有達到corePoolSize的值,則新建線程(核心線程)執行任務,如果達到了,則入隊等候,如果隊列已滿,則新建線程(非核心線程)執行任務,又如果總線程數到了maximumPoolSize,並且隊列也滿了,則發生錯誤
    DelayQueue
    隊列內元素必須實現Delayed接口,這就意味着你傳進去的任務必須先實現Delayed接口。這個隊列接收到任務時,首先先入隊,只有達到了指定的延時時間,纔會執行任務
  • ThreadFactory threadFactory,創建線程的方式,這是一個接口,你new他的時候需要實現他的Thread newThread(Runnable r)方法,一般用不上
  • RejectedExecutionHandler handler,用於拋出異常

新建一個線程池時,一般只用5個參數的構造函數。
(3.2)添加任務
通過ThreadPoolExecutor.execute(Runnable command)方法即可向線程池內添加一個任務
(3.3)執行策略

  • 線程數量未達到corePoolSize,則新建一個線程(核心線程)執行任務
  • 線程數量達到了corePools,則將任務移入隊列等待
  • 隊列已滿,新建線程(非核心線程)執行任務
  • 隊列已滿,總線程數又達到了maximumPoolSize,就會由上面handler(RejectedExecutionHandler)拋出異常
  1. 線程池類型
    Java通過Executors提供了四種線程池,這四種線程池都是直接或間接配置ThreadPoolExecutor的參數實現的。

(1)CachedThreadPool()
可緩存線程池:

  • 線程數無限制
  • 有空閒線程則複用空閒線程,若無空閒線程則新建線程
  • 一定程序減少頻繁創建/銷燬線程,減少系統開銷

創建方法:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

源碼:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

(2)FixedThreadPool()
定長線程池:

  • 可控制線程最大併發數(同時執行的線程數)
  • 超出的線程會在隊列中等待

創建方法:

//nThreads => 最大線程數即maximumPoolSize
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);

//threadFactory => 創建線程的方法,這就是我叫你別理他的那個星期六!你還看!
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory);

源碼:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

(3)ScheduledThreadPool()
定長線程池:

  • 支持定時及週期性任務執行。

創建方法:

//nThreads => 最大線程數即maximumPoolSize
ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);

源碼:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

//ScheduledThreadPoolExecutor():
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

(4)SingleThreadExecutor()
單線程化的線程池:

  • 有且僅有一個工作線程執行任務
  • 所有任務按照指定順序執行,即遵循隊列的入隊出隊規則

創建方法:

ExecutorService singleThreadPool = Executors.newSingleThreadPool();

源碼:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  1. Java 線程池的停止
    Executor框架提供了Java線程池的能力,ExecutorService擴展了Executor,提供了管理線程生命週期的關鍵能力。其中,ExecutorService.submit返回了Future對象來描述一個線程任務,它有一個cancel()方法。
    下面的例子擴展了上面的InterruptedExample,要求線程在限定時間內得到結果,否則觸發超時停止。
public class InterruptByFuture {

    public static void main(String[] args) throws Exception {
        ExecutorService es = Executors.newSingleThreadExecutor();
        Future<?> task = es.submit(new MyThread());

        try {
            //限定時間獲取結果
            task.get(5, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            //超時觸發線程中止
            System.out.println("thread over time");
        } catch (ExecutionException e) {
            throw e;
        } finally {
            boolean mayInterruptIfRunning = true;
            task.cancel(mayInterruptIfRunning);
        }
    }

    private static class MyThread extends Thread {

        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {   
                try {
                    System.out.println("count");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("interrupt");
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("thread stop");
        }

        public void cancel() {
            interrupt();
        }
    }
}

Future的get方法可以傳入時間,如果限定時間內沒有得到結果,將會拋出TimeoutException。此時,可以調用Future的cancel()方法,對任務所在線程發出中斷請求。
cancel()有個參數mayInterruptIfRunning,表示任務是否能夠接收到中斷。
mayInterruptIfRunning=true時,任務如果在某個線程中運行,那麼這個線程能夠被中斷;
mayInterruptIfRunning=false時,任務如果還未啓動,就不要運行它,應用於不處理中斷的任務
要注意,mayInterruptIfRunning=true表示線程能接收中斷,但線程是否實現了中斷不得而知。線程要正確響應中斷,才能真正被cancel。
線程池的shutdownNow()會嘗試停止池內所有在執行的線程,原理也是發出中斷請求。

1.5)Java 併發模型

(1)併發模型
(1.1)併發與並行
併發程序是指在運行中有兩個及以上的任務同時在處理,與之相關的概念並行,是指在運行中有兩個及以上的任務同時執行,差別是在於處理和執行。在單核CUP中兩個及以上任務的處理方式是讓它們交替的進入CUP執行,這種對執行的處理方式就是併發。
並行只能發生在多核CUP中,每個CUP核心拿到一個任務同時執行,並行是併發的一個子集
與串行程序相比並發編程的優點:
(1)提高硬件資源的利用率(特別是IO資源),提高系統的響應速度、減少客戶端等待、增加系統吞吐量
(2)解決特定領域的問題,比如GUI系統,WEB服務器等。
(1.2)線程併發實現
併發實現包括三種:進程併發、線程併發與協程併發。
其中線程併發是Java的併發模型方式。
在操作系統中線程是包含在進程中的消費資源較少、運行迅速的最小執行單元,根據操作系統內核是否對線程可感知,把線程分爲內核線程和用戶線程。

  • 基於內核線程
    使用內核線程的一種高級接口–輕量級進程(Light Weight Process,LWP)實現的線程(通常意義上的線程),它與內核線程是一對一的關係。線程的創建,初始化,同步,切換(用戶態、內核態)都需要內核調度器(Scheduler)進行調度,消耗內核資源,每一個輕量級進程都需要一個內核線程對應,所以這線程能創建的數量是也是有限的。
  • 基於用戶線程
    建立在用戶空間的上的線程,內核對此無感知。線程的創建、調度在用戶態完成,不需要系統內核支援。由於沒有系統內核的支援,所有的線程操作都需要用戶程序自己處理。線程的創建、切換和調度都是需要考慮的問題,而且由於操作系統只把處理器資源分配到進程,如“阻塞如何處理”,“多處理器系統中如何將線程映射到其它處理器上”這類問題解決起來將會異常困難,甚至不可能完成。
  • 基於用戶線程和內核線程混合
    即使用內核線程(輕量級進程),也使用用戶線程。用戶線程依然建立在用戶空間上,線程的創建、調度、處理器映射能夠得到內核線程的支援,實現簡單。用戶線程與輕量級進程(內核線程)是N:M的對應關係,可以支持大規模的併發。

(1.3)線程併發通信
線程間通過協作才能合力完成一個任務,協作的基礎就是通信。常用的線程間通信的方式有兩種。

  • 共享內存
    設置一個共享變量,多個線程都可以對共享變量進行操作,共享變量就行通信的中介。共享內存通信方式實現簡單,數據的共享還使得線程間的通信不需要數據的傳送,直接對內存進行訪問,加快了程序的執行效率。
    但是多個線程操作同一個共享變量,勢必會造成“數據爭用”。競爭條件下必須讓共享變量進入臨界區進行保護,否則會產生數據不一致。
  • 消息傳遞

(2)Java併發模型——線程模型
(2.1)簡介
每一個JAVA線程都對應者一個內核線程,所以線程的創建、調度、上下文切換都需要系統內核的支持,會消耗系統資源。
JAVA線程間通信是通過共享內存實現的,鎖在線程併發中有着舉足輕重地位,使用JAVA多線程時需要非常小心的處理同步問題。

(2.2)問題
Java併發編程需要面對兩個問題:

  • 資源消耗問題:
    包括線程的創建、上下文切換對資源的消耗,鎖的互斥操作對資源的消耗,常用的解決方法有池化資源,根據計算類型保有適量線程,鎖優化策略等。
  • 線程安全問題:
    線程安全問題,要想讓併發程序正確的執行,需要解決原子性,可見性、有序性的問題,常用的保障線程安全的方法有加鎖、不共享狀態、不可變對象。
    “線程與鎖”模型是JAVA語言的併發模型。這也是大多數語言都支持的模型,由於其基本接近硬件本身運行的模式,可以解決的問題領域很多有着很高的運行效率,一直都是併發編程的首選。缺點是使用這樣模型需要開發者時刻警惕線程安全問題,處理複雜的線程協作問題,關注計算資源的開銷問題。

(2.3)併發機制

  1. 鎖機制
    比如synchronized或者ReentrantLock
  2. CAS算法
    讀的時候記錄結果,寫的時候檢查是不是還是剛纔讀到的,如果是,那麼說明讀和寫之間沒有其他線程修改它的值,這段代碼是原子執行的,可以進行修改操作;如果不是,那麼說明其他線程修改了它的值,這段代碼並沒有原子執行,此時需要使用循環,重新讀取,再檢查,直至保證原子執行。如Volatile。
    這種方式和鎖有一些類似,都可以保證代碼的原子執行,但是使用鎖會涉及到一些線程的掛起和上下文切換問題,需要消耗資源,但是CAS僅是輪詢,不涉及JVM級別。書中提到低度和中度競爭的情況下,CAS的代價是低於鎖的,在高度競爭的情況下,CAS的代價是高於鎖的(畢竟輪詢也需要消耗資源,佔用CPU),但高度競爭這種情況是比較少的。在一些細粒度的併發操作上,推薦還是使用CAS。

(2.4)併發工具

  1. 基礎類:Synchronized、Volatile、Final
  2. java.util.concurrent包:原子類(atomic)、顯示鎖(ReentrantLock)、同步模式(CountDownLatch)、線程安全容器(ConcurrentHashMap、CopyOnWriteArrayList、Queue、TransferQueue)
    感謝Doug Lea在Java 5中提供了他里程碑式的傑作java.util.concurrent包,它的出現讓Java的併發編程有了更多的選擇和更好的工作方式。Doug Lea的傑作主要包括以下內容:
  • 更好的線程安全的容器
  • 線程池和相關的工具類
  • 可選的非阻塞解決方案
  • 顯示的鎖和信號量機制

1.6)Java 線程安全

(1)什麼是Java線程安全
多個線程不管以何種方式訪問某個類,並且在主調代碼中不需要進行同步,都能表現正確的行爲。
線程安全有以下幾種實現方式
(2)如何保證Java線程安全

  1. 不可變
    不可變(Immutable)的對象一定是線程安全的,不需要再採取任何的線程安全保障措施。只要一個不可變的對象被正確地構建出來,永遠也不會看到它在多個線程之中處於不一致的狀態。多線程環境下,應當儘量使對象成爲不可變,來滿足線程安全。
    不可變的類型:
  • final 關鍵字修飾的基本數據類型
  • String
  • 枚舉類型
  • Number 部分子類,如 Long 和 Double 等數值包裝類型,BigInteger 和 BigDecimal 等大數據類型。但同爲 Number 的原子類 AtomicInteger 和 AtomicLong 則是可變的。
  1. 互斥同步
  • synchronized
  • ReentrantLock。
  1. 非阻塞同步
    互斥同步屬於一種悲觀的併發策略,總是認爲只要不去做正確的同步措施,那就肯定會出現問題。無論共享數據是否真的會出現競爭,它都要進行加鎖(這裏討論的是概念模型,實際上虛擬機會優化掉很大一部分不必要的加鎖)、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的線程需要喚醒等操作。
  • CAS
    隨着硬件指令集的發展,我們可以使用基於衝突檢測的樂觀併發策略:先進行操作,如果沒有其它線程爭用共享數據,那操作就成功了,否則採取補償措施(不斷地重試,直到成功爲止)。這種樂觀的併發策略的許多實現都不需要將線程阻塞,因此這種同步操作稱爲非阻塞同步。
    樂觀鎖需要操作和衝突檢測這兩個步驟具備原子性,這裏就不能再使用互斥同步來保證了,只能靠硬件來完成。硬件支持的原子性操作最典型的是:比較並交換(Compare-and-Swap,CAS)。CAS 指令需要有 3 個操作數,分別是內存地址 V、舊的預期值 A 和新值 B。當執行操作時,只有當 V 的值等於 A,纔將 V 的值更新爲 B。
  • AtomicInteger
  • ABA
  1. 無同步方案
  • 棧自閉
    多個線程訪問同一個方法的局部變量時,不會出現線程安全問題,因爲局部變量存儲在虛擬機棧中,屬於線程私有的。
  • 線程本地存儲(ThreadLocal)
  • 可重入代碼(Reentrant Code)
    這種代碼也叫做純代碼(Pure Code),可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼(包括遞歸調用它本身),而在控制權返回後,原來的程序不會出現任何錯誤。

1.7)Java 斷點續傳

(1)原理
在下載行爲出現中斷的時候,記錄下中斷的位置信息,然後在下次行爲開始的時候,直接從記錄的這個位置開始下載內容,而不再從頭開始。
分爲兩步:

  • 當“上傳(下載)的行爲”出現中斷,我們需要記錄本次上傳(下載)的位置(position)。
  • 當“續”這一行爲開始,我們直接跳轉到postion處繼續上傳(下載)的行爲。

(2)代碼

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

public class Test {
// step1:首先,我們定義了一個變量position,記錄在發生中斷的時候,已完成讀寫的位置。(這是爲了方便,實際來說肯定應該講這個值存到文件或者數據庫等進行持久化)
    private static int position = -1;

    public static void main(String[] args) {
        // 源文件與目標文件
        File sourceFile = new File("D:/", "test.txt");
        File targetFile = new File("E:/", "test.txt");
        // 輸入輸出流
        FileInputStream fis = null;
        FileOutputStream fos = null;
        // 數據緩衝區
        byte[] buf = new byte[1];

        try {
            fis = new FileInputStream(sourceFile);
            fos = new FileOutputStream(targetFile);
            // 數據讀寫
            while (fis.read(buf) != -1) {
                fos.write(buf);
// step2:然後在文件讀寫的while循環中,我們去模擬一箇中斷行爲的發生。這裏是當targetFile的文件長度爲3個字節則模擬拋出一個我們自定義的異常。(我們可以想象爲實際下載中,已經上傳(下載)了”x”個字節的內容,這個時候網絡中斷了,那麼我們就在網絡中斷拋出的異常中將”x”記錄下來)。
                if (targetFile.length() == 3) {
                    position = 3;
                    throw new FileAccessException();
                }
            }
        } catch (FileAccessException e) {
//step3:開啓”續傳“行爲,即keepGoing方法.
            keepGoing(sourceFile,targetFile, position);
        } catch (FileNotFoundException e) {
            System.out.println("指定文件不存在");
        } catch (IOException e) {
            // TODO: handle exception
        } finally {
            try {
                // 關閉輸入輸出流
                if (fis != null)
                    fis.close();

                if (fos != null)
                    fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

    private static void keepGoing(File source,File target, int position) {
// step3.1:我們起頭讓線程休眠10秒鐘,這正是爲了讓我們運行程序看到效果。     
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
// step3.2:在“續傳”行爲開始後,通過RandomAccessFile類來包裝我們的文件,然後通過seek將指針指定到之前發生中斷的位置進行讀寫就搞定了。 
(實際的文件下載上傳,我們當然需要將保存的中斷值上傳給服務器,這個方式通常爲
        try {
            RandomAccessFile readFile = new RandomAccessFile(source, "rw");
            RandomAccessFile writeFile = new RandomAccessFile(target, "rw");
            readFile.seek(position);
            writeFile.seek(position);

            // 數據緩衝區
            byte[] buf = new byte[1];
            // 數據讀寫
            while (readFile.read(buf) != -1) {
                writeFile.write(buf);
            }
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

class FileAccessException extends Exception {

}

(3)實現結果
運行程序,那麼文件就會開啓“由D盤上傳到E盤的過程”,我們首先點開E盤,會發現的確多了一個test.txt文件,打開它發現內容如下:
在這裏插入圖片描述
這個時候我們發現內容只有“abc”。這是在我們預料以內的,因爲我們的程序模擬在文件上傳了3個字節的時候發生了中斷。
等待10秒鐘過去,然後再點開該文件,發現內容的確已經變成了“abc”,由此也就完成了續傳。
在這裏插入圖片描述

(二)Android 多線程開發

在這裏插入圖片描述

2.1)基礎使用

1、繼承Thread類

(1)簡介
Thread類是Java中實現多線程的具體類,封裝了所需線程操作。在Android開發中用於實現多線程。
注:線程對象&運行線程區別
線程對象是運行線程的實體,用來控制線程行爲的唯一方式。線程對象通過線程類實例化創建,負責控制線程的狀態,如:運行、睡眠、掛起/停止。
優點
實現簡單:只要繼承Thread類&複寫run()即可實現多線程操作
缺點
侷限性大:必須集成Thread類(Java規定單繼承,即集成Thread類後不可繼承其他類)
不適合資源共享:一個線程=一個實例對象,相對獨立無法資源共享
消耗資源:Thread線程=一次性消費品&一個耗時任務。執行完一個耗時操作後,線程會被自動銷燬,如果有100個耗時任務則必須開100個線程。多次創建&銷燬線程,耗費系統資源
(2)使用
2.1)使用步驟
在這裏插入圖片描述
2.2)常規使用

// 步驟1:創建線程類 (繼承自Thread類)
   class MyThread extends Thread{

// 步驟2:複寫run(),內容 = 定義線程行爲
    @Override
    public void run(){
    ... // 定義的線程行爲
    }
}

// 步驟3:創建線程對象,即 實例化線程類
  MyThread mt=new MyThread(“線程名稱”);

// 步驟4:通過 線程對象 控制線程的狀態,如 運行、睡眠、掛起  / 停止
// 此處採用 start()開啓線程
  mt.start();

2.3)匿名類使用

// 步驟1:採用匿名類,直接 創建 線程類的實例
 new Thread("線程名稱") {
                 // 步驟2:複寫run(),內容 = 定義線程行爲
                    @Override
                    public void run() {       
                  // 步驟3:通過 線程對象 控制線程的狀態,如 運行、睡眠、掛起  / 停止   
                      }.start();

2.4)常規&匿名類使用區別
在這裏插入圖片描述

2、實現Runnable接口

(1)簡介
一個與多線程相關的抽象接口,僅定義1個方法=run(),在Android開發中用於實現多線程
適合資源共享:Runnable可被多個線程(Thread實例)共享,適合多線程處理同一資源的情況
靈活:一個類可以繼承多個接口,避免集成THread類導致的單繼承侷限性

補充:Java進階知識——接口與繼承區別
(1)關鍵字:接口interface,繼承extends
(2)定義:接口:對功能的描述,繼承:具體描述一種類
(3)結構:接口只能定義全局常量、抽象方法,繼承可以定義屬性方法、常量、變量等等
(4)接口可以實現"多繼承",繼承只能"單繼承"
(5)實現接口的類一定要實現接口的抽象方法,繼承的類可以調用、重載父類的任意方法

(2)使用
2.1)使用步驟
在這裏插入圖片描述
注:
Java中真正能創建新線程的只有Thread類對象
通過實現Runnable的方式,最終還是通過Thread類對象來創建線程
所以對於 實現了Runnable接口的類,稱爲 線程輔助類;Thread類纔是真正的線程類
2.2)常規使用

// 步驟1:創建線程輔助類,實現Runnable接口
 class MyThread implements Runnable{
    ....
    @Override
// 步驟2:複寫run(),定義線程行爲
    public void run(){

    }
}

// 步驟3:創建線程輔助對象,即 實例化 線程輔助類
  MyThread mt=new MyThread();

// 步驟4:創建線程對象,即 實例化線程類;線程類 = Thread類;
// 創建時通過Thread類的構造函數傳入線程輔助類對象
// 原因:Runnable接口並沒有任何對線程的支持,我們必須創建線程類(Thread類)的實例,從Thread類的一個實例內部運行
  Thread td=new Thread(mt);

// 步驟5:通過 線程對象 控制線程的狀態,如 運行、睡眠、掛起  / 停止
// 當調用start()方法時,線程對象會自動回調線程輔助類對象的run(),從而實現線程操作
  td.start();

2.3)匿名類使用

    // 步驟1:通過匿名類 直接 創建線程輔助對象,即 實例化 線程輔助類
    Runnable mt = new Runnable() {
                    // 步驟2:複寫run(),定義線程行爲
                    @Override
                    public void run() {
                    }
                };

                // 步驟3:創建線程對象,即 實例化線程類;線程類 = Thread類;
                Thread mt1 = new Thread(mt, "窗口1");
           
                // 步驟4:通過 線程對象 控制線程的狀態,如 運行、睡眠、掛起  / 停止
                mt1.start();

(3)繼承Thread類&實現Runnable接口對比
在這裏插入圖片描述
Android中Thread/Runnable方法使用了java原生的Thread/Runnable的線程形態,詳細用法可參考上文Java多線程開發。

3、Handler

見陳小云Android學習之旅:第十章 進程間的通信 之 Handler機制(二)

2.2)複合使用

1、AsyncTask

(1)簡介
一個Android已封裝好的輕量級異步類,屬於抽象類,使用時需要實現子類。用於
實現多線程,如在工作線程中執行耗時任務
異步通信、消息傳遞,如實現工作線程&主線程(UI線程)之間的通信,即:將工作線程的執行結果傳遞給主線程,從而在主線程中指向相關UI操作(保證線程安全)

public abstract class AsyncTask<Params, Progress, Result> { 
 ... 
 }

不需使用"任務線程(如Thread類)+Handler"複雜組合,方便實現異步通信
採用線程池的緩存線程+複用線程,避免頻繁創建&銷燬線程所帶來的系統資源開銷
(2)類定義
AsyncTask類屬於抽象類,即使用時需 實現子類

public abstract class AsyncTask<Params, Progress, Result> { 
 ... 
}
// 類中參數爲3種泛型類型
// 整體作用:控制AsyncTask子類執行線程任務時各個階段的返回類型
// 具體說明:
    // a. Params:開始異步任務執行時傳入的參數類型,對應excute()中傳遞的參數
    // b. Progress:異步任務執行過程中,返回下載進度值的類型
    // c. Result:異步任務執行完成後,返回的結果類型,與doInBackground()的返回值類型保持一致
// 注:
    // a. 使用時並不是所有類型都被使用
    // b. 若無被使用,可用java.lang.Void類型代替
    // c. 若有不同業務,需額外再寫1個AsyncTask的子類
}

(3)核心方法
AsyncTask 核心 & 常用的方法如下:
在這裏插入圖片描述
方法執行順序如下
在這裏插入圖片描述
(4)使用步驟
步驟1:創建AsyncTask子類
a. 繼承AsyncTask類
b. 爲3個泛型參數指定類型;若不使用,可用java.lang.Void類型代替
c. 根據需求,在AsyncTask子類內實現核心方法

  private class MyTask extends AsyncTask<Params, Progress, Result> {

        ....

      // 方法1:onPreExecute()
      // 作用:執行 線程任務前的操作
      // 注:根據需求複寫
      @Override
      protected void onPreExecute() {
           ...
        }

      // 方法2:doInBackground()
      // 作用:接收輸入參數、執行任務中的耗時操作、返回 線程任務執行的結果
      // 注:必須複寫,從而自定義線程任務
      @Override
      protected String doInBackground(String... params) {

            ...// 自定義的線程任務

            // 可調用publishProgress()顯示進度, 之後將執行onProgressUpdate()
             publishProgress(count);
              
         }

      // 方法3:onProgressUpdate()
      // 作用:在主線程 顯示線程任務執行的進度
      // 注:根據需求複寫
      @Override
      protected void onProgressUpdate(Integer... progresses) {
            ...

        }

      // 方法4:onPostExecute()
      // 作用:接收線程任務執行結果、將執行結果顯示到UI組件
      // 注:必須複寫,從而自定義UI操作
      @Override
      protected void onPostExecute(String result) {

         ...// UI操作

        }

      // 方法5:onCancelled()
      // 作用:將異步任務設置爲:取消狀態
      @Override
        protected void onCancelled() {
        ...
        }
  }

步驟2:創建Async子類的實例對象(任務實例)
AsyncTask子類的實例必須在UI線程中創建

  MyTask mTask = new MyTask();

步驟3:手動調用execute()從而執行異步線程任務
a.必須在UI線程中調用
b.同一個AsyncTask實例對象只能執行1次,若執行第2次會拋出異常
c.執行任務中,系統會系統會自動調用AsyncTask的一系列方法:onPreExecute() 、doInBackground()、onProgressUpdate() 、onPostExecute()
d. 不能手動調用上述方法

mTask.execute();

(5)實例分析
5.1)實例需求
點擊按鈕 則 開啓線程執行線程任務
顯示後臺加載進度
加載完畢後更新UI組件
期間若點擊取消按鈕,則取消加載
5.2)代碼實現

    private class MyTask extends AsyncTask<String, Integer, String> {

        // 方法1:onPreExecute()
        // 作用:執行 線程任務前的操作
        @Override
        protected void onPreExecute() {
            text.setText("加載中");
            // 執行前顯示提示
        }


        // 方法2:doInBackground()
        // 作用:接收輸入參數、執行任務中的耗時操作、返回 線程任務執行的結果
        // 此處通過計算從而模擬“加載進度”的情況
        @Override
        protected String doInBackground(String... params) {

            try {
                int count = 0;
                int length = 1;
                while (count<99) {

                    count += length;
                    // 可調用publishProgress()顯示進度, 之後將執行onProgressUpdate()
                    publishProgress(count);
                    // 模擬耗時任務
                    Thread.sleep(50);
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }

            return null;
        }

        // 方法3:onProgressUpdate()
        // 作用:在主線程 顯示線程任務執行的進度
        @Override
        protected void onProgressUpdate(Integer... progresses) {

            progressBar.setProgress(progresses[0]);
            text.setText("loading..." + progresses[0] + "%");

        }

        // 方法4:onPostExecute()
        // 作用:接收線程任務執行結果、將執行結果顯示到UI組件
        @Override
        protected void onPostExecute(String result) {
            // 執行完畢後,則更新UI
            text.setText("加載完畢");
        }

        // 方法5:onCancelled()
        // 作用:將異步任務設置爲:取消狀態
        @Override
        protected void onCancelled() {

            text.setText("已取消");
            progressBar.setProgress(0);

        }
    }

(6)問題&解決
6.1)關於生命週期
問題:AsyncTask不與任何組件綁定生命週期
解決:在Activity 或 Fragment中使用 AsyncTask時,最好在Activity 或 Fragment的onDestory()調用 cancel(boolean);
6.2)關於內存泄露
問題:若AsyncTask被聲明爲Activity的非靜態內部類,當Activity需銷燬時,會因AsyncTask保留對Activity的引用 而導致Activity無法被回收,最終引起內存泄露
解決:AsyncTask應被聲明爲Activity的靜態內部類
線程任務執行結果丟失
問題:當Activity重新創建時(屏幕旋轉 / Activity被意外銷燬時後恢復),之前運行的AsyncTask(非靜態的內部類)持有的之前Activity引用已無效,故複寫的onPostExecute()將不生效,即無法更新UI操作
解決:在Activity恢復時的對應方法 重啓 任務線程
(7)源碼分析
7.1)原理介紹
AsyncTask的實現原理 = 線程池 + Handler
其中:線程池用於線程調度、複用 & 執行任務;Handler 用於異步通信
其內部封裝了2個線程池 + 1個Handler,具體介紹如下:
在這裏插入圖片描述
7.2)源碼分析
根據AsyncTask使用步驟講解
步驟1:創建AsbncTask子類
該類複寫的方法在後續源碼中調用
步驟2:創建AsyncTask子類的實例對象(任務實例)
1、具體使用

  MyTask mTask = new MyTask();

2、源碼分析:AsyncTask的構造函數

/**
  * 源碼分析:AsyncTask的構造函數
  */
  public AsyncTask() {
        // 1. 初始化WorkerRunnable變量 = 一個可存儲參數的Callable對象 ->>分析1
        mWorker = new WorkerRunnable<Params, Result>() {
            // 在任務執行線程池中回調:THREAD_POOL_EXECUTOR.execute()
            // 下面會詳細講解
            public Result call() throws Exception {

                // 添加線程的調用標識
                mTaskInvoked.set(true); 

                Result result = null;
                try {
                    // 設置線程的優先級
                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                    
                    // 執行異步操作 = 耗時操作
                    // 即 我們使用過程中複寫的耗時任務
                    result = doInBackground(mParams);

                    Binder.flushPendingCommands();
                } catch (Throwable tr) {
                    
                    mCancelled.set(true);// 若運行異常,設置取消的標誌
                    throw tr;
                } finally {
                    
                    // 把異步操作執行的結果發送到主線程
                    // 從而更新UI,下面會詳細講解
                    postResult(result); 
                }
                return result;
            }
        };

        // 2. 初始化FutureTask變量 = 1個FutureTask ->>分析2
        mFuture = new FutureTask<Result>(mWorker) {

            // done()簡介:FutureTask內的Callable執行完後的調用方法
            // 作用:複查任務的調用、將未被調用的任務的結果通過InternalHandler傳遞到UI線程
            @Override
            protected void done() {
                try {

                    // 在執行完任務後檢查,將沒被調用的Result也一併發出 ->>分析3
                    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) {

                    //若 發生異常,則將發出null
                    postResultIfNotInvoked(null);
                }
            }
        };
    }

/**
  * 分析1:WorkerRunnable類的構造函數
  */
  private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        // 此處的Callable也是任務;
        // 與Runnable的區別:Callable<T>存在返回值 = 其泛型
        Params[] mParams;
    }

/**
  * 分析2:FutureTask類的構造函數
  * 定義:1個包裝任務的包裝類
  * 注:內部包含Callable<T> 、增加了一些狀態標識 & 操作Callable<T>的接口
  */
  public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;      
    }
    // 回到調用原處

/**
  * 分析3:postResultIfNotInvoked()
  */
  private void postResultIfNotInvoked()(Result result) {
        // 取得任務標記
        final boolean wasTaskInvoked = mTaskInvoked.get();

        // 若任務無被執行,將未被調用的任務的結果通過InternalHandler傳遞到UI線程
        if (!wasTaskInvoked) {
            postResult(result);
        }
    }

3、總結
創建了1個WorkerRunnable類 的實例對象 & 複寫了call()方法
創建了1個FutureTask類 的實例對象 & 複寫了 done()
步驟3:手動調用execute(Params…params)
1、具體使用

mTask.execute();

2、源碼分析

public final AsyncTask<Params, Progress, Result> execute(Params... params) {

        return executeOnExecutor(sDefaultExecutor, params);
        // ->>分析1

    }

 /**
  * 分析1:executeOnExecutor(sDefaultExecutor, params)
  * 參數說明:sDefaultExecutor = 任務隊列 線程池類(SerialExecutor)的對象
  */
  public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,Params... params) {

        // 1. 判斷 AsyncTask 當前的執行狀態
        // PENDING = 初始化狀態
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }

        // 2. 將AsyncTask狀態設置爲RUNNING狀態
        mStatus = Status.RUNNING;

        // 3. 主線程初始化工作
        onPreExecute();

        // 4. 添加參數到任務中
        mWorker.mParams = params;

        // 5. 執行任務
        // 此處的exec = sDefaultExecutor = 任務隊列 線程池類(SerialExecutor)的對象
        // ->>分析2
        exec.execute(mFuture);
        return this;
    }

/**
  * 分析2:exec.execute(mFuture)
  * 說明:屬於任務隊列 線程池類(SerialExecutor)的方法
  */
  private static class SerialExecutor implements Executor {
        // SerialExecutor = 靜態內部類
        // 即 是所有實例化的AsyncTask對象公有的

        // SerialExecutor 內部維持了1個雙向隊列;
        // 容量根據元素數量調節
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        // execute()被同步鎖synchronized修飾
        // 即說明:通過鎖使得該隊列保證AsyncTask中的任務是串行執行的
        // 即 多個任務需1個個加到該隊列中;然後 執行完隊列頭部的再執行下一個,以此類推
        public synchronized void execute(final Runnable r) {
            // 將實例化後的FutureTask類 的實例對象傳入
            // 即相當於:向隊列中加入一個新的任務
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();->>分析3
                    }
                }
            });
            // 若當前無任務執行,則去隊列中取出1個執行
            if (mActive == null) {
                scheduleNext();
            }
        }
        // 分析3
        protected synchronized void scheduleNext() {
            // 1. 取出隊列頭部任務
            if ((mActive = mTasks.poll()) != null) {

                // 2. 執行取出的隊列頭部任務
                // 即 調用執行任務線程池類(THREAD_POOL_EXECUTOR)->>繼續往下看
                THREAD_POOL_EXECUTOR.execute(mActive);
                
            }
        }
    }

3、總結:
執行任務前,通過 任務隊列 線程池類(SerialExecutor)將任務按順序放入到隊列中;
通過同步鎖 修飾execute()從而保證AsyncTask中的任務是串行執行的,之後的線程任務執行是 通過任務線程池類(THREAD_POOL_EXECUTOR) 進行的。
繼續往下分析:THREAD_POOL_EXECUTOR.execute()

/**
  * 源碼分析:THREAD_POOL_EXECUTOR.execute()
  * 說明:
  *     a. THREAD_POOL_EXECUTOR實際上是1個已配置好的可執行並行任務的線程池
  *     b. 調用THREAD_POOL_EXECUTOR.execute()實際上是調用線程池的execute()去執行具體耗時任務
  *     c. 而該耗時任務則是步驟2中初始化WorkerRunnable實例對象時複寫的call()
  * 注:下面先看任務執行線程池的線程配置過程,看完後請回到步驟2中的源碼分析call()
  */

    // 步驟1:參數設置
        //獲得當前CPU的核心數
        private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
        //設置線程池的核心線程數2-4之間,但是取決於CPU核數
        private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
        //設置線程池的最大線程數爲 CPU核數*2+1
        private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
        //設置線程池空閒線程存活時間30s
        private static final int KEEP_ALIVE_SECONDS = 30;

        //初始化線程工廠
        private static final ThreadFactory sThreadFactory = new ThreadFactory() {
            private final AtomicInteger mCount = new AtomicInteger(1);

            public Thread newThread(Runnable r) {
                return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
            }
        };

        //初始化存儲任務的隊列爲LinkedBlockingQueue 最大容量爲128
        private static final BlockingQueue<Runnable> sPoolWorkQueue =
                new LinkedBlockingQueue<Runnable>(128);

    // 步驟2: 根據參數配置執行任務線程池,即 THREAD_POOL_EXECUTOR
    public static final Executor THREAD_POOL_EXECUTOR;

    static {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                sPoolWorkQueue, sThreadFactory);
        // 設置核心線程池的 超時時間也爲30s
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        THREAD_POOL_EXECUTOR = threadPoolExecutor;
    }

    // 請回到步驟2中的源碼分析call()

至此,我們回到步驟2中的源碼分析call()

/**
  * 步驟2的源碼分析:AsyncTask的構造函數
  */
    public AsyncTask() {
        // 1. 初始化WorkerRunnable變量 = 一個可存儲參數的Callable對象
        mWorker = new WorkerRunnable<Params, Result>() {

            public Result call() throws Exception {

                // 添加線程的調用標識
                mTaskInvoked.set(true); 

                Result result = null;
                try {
                    // 設置線程的優先級
                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                    
                    // 執行異步操作 = 耗時操作
                    // 即 我們使用過程中複寫的耗時任務
                    result = doInBackground(mParams);

                    Binder.flushPendingCommands();
                } catch (Throwable tr) {
                    
                    mCancelled.set(true);// 若運行異常,設置取消的標誌
                    throw tr;
                } finally {
                    
                    // 把異步操作執行的結果發送到主線程
                    // 從而更新UI ->>分析1
                    postResult(result); 
                }
                return result;
            }
        };

        .....// 省略
    }
/**
  * 分析1:postResult(result)
  */
   private Result postResult(Result result) {

        @SuppressWarnings("unchecked")

        // 創建Handler對象 ->> 源自InternalHandler類—>>分析2
        Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        // 發送消息到Handler中
        message.sendToTarget();
        return result;

    }


/**
  * 分析2:InternalHandler類
  */
    private static class InternalHandler extends Handler {

        // 構造函數
        public InternalHandler() {
            super(Looper.getMainLooper());
            // 獲取的是主線程的Looper()
            // 故 AsyncTask的實例創建 & execute()必須在主線程使用
        }

        @Override
        public void handleMessage(Message msg) {

            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;

            switch (msg.what) {
                // 若收到的消息 = MESSAGE_POST_RESULT
                // 則通過finish() 將結果通過Handler傳遞到主線程
                case MESSAGE_POST_RESULT:
                    result.mTask.finish(result.mData[0]); ->>分析3
                    break;

                // 若收到的消息 = MESSAGE_POST_PROGRESS
                // 則回調onProgressUpdate()通知主線程更新進度的操作
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }
/**
  * 分析3:result.mTask.finish(result.mData[0])
  */
  private void finish(Result result) {
        // 先判斷是否調用了Cancelled()
            // 1. 若調用了則執行我們複寫的onCancelled()
            // 即 取消任務時的操作
            if (isCancelled()) {
                onCancelled(result);
            } else {

            // 2. 若無調用Cancelled(),則執行我們複寫的onPostExecute(result)
            // 即更新UI操作
                onPostExecute(result);
            }
            // 注:不管AsyncTask是否被取消,都會將AsyncTask的狀態變更爲:FINISHED
            mStatus = Status.FINISHED;
        }

總結
任務線程池類(THREAD_POOL_EXECUTOR)實際上是1個已配置好的可執行並行任務的線程池
調用THREAD_POOL_EXECUTOR.execute()實際上是調用線程池的execute()去執行具體耗時任務
而該耗時任務則是步驟2中初始化 WorkerRunnable實例對象時複寫的call()內容
在call()方法裏,先調用 我們複寫的doInBackground(mParams)執行耗時操作
再調用postResult(result), 通過 InternalHandler 類 將任務消息傳遞到主線程;根據消息標識(MESSAGE_POST_RESULT)判斷,最終通過finish()調用我們複寫的onPostExecute(result),從而實現UI更新操作
7.3)Async源碼總結
在這裏插入圖片描述

2、HandlerThread

(1)介紹
HandlerThread是一個Android已封裝好的輕量級異步類,用於實現多線程(在工作線程中執行耗時任務)及異步通信、消息傳遞(工作線程&主線程之間通信)從而保證線程安全
HandlerThread本質上是通過繼承Thread類和封裝Handler類的使用,從而使得創建新線程和與其他線程進行通信變得更加方便易用(不需要使用"任務線程(如繼承Thread類)+Handler"複雜組合)
(2)使用

步驟1:創建HandlerThread實例對象
//傳入參數 = 線程名字,作用 = 標記該線程
HandlerThread mHandlerThread = new HandlerThread("handlerThread");
步驟2:啓動線程
mHandlerThread.start()
步驟3:創建工作線程Handler&複寫handleMessage()
// 作用:關聯HandlerThread的Looper對象、實現消息處理操作 & 與其他線程進行通信
// 注:消息處理操作(HandlerMessage())的執行線程 = mHandlerThread所創建的工作線程中執行
  Handler workHandler = new Handler( handlerThread.getLooper() ) {
            @Override
            public boolean handleMessage(Message msg) {
                ...//消息處理
                return true;
            }
        });
步驟4:使用工作線程Handler向工作線程的消息隊列發送消息
// 在工作線程中,當消息循環時取出對應消息 & 在工作線程執行相關操作
  // a. 定義要發送的消息
  Message msg = Message.obtain();
  msg.what = 2; //消息的標識
  msg.obj = "B"; // 消息的存放
  // b. 通過Handler發送消息到其綁定的消息隊列
  workHandler.sendMessage(msg);
步驟5:結束線程,即停止線程的消息循環
  mHandlerThread.quit();

(3)實例

public class MainActivity extends AppCompatActivity {

    Handler mainHandler,workHandler;
    HandlerThread mHandlerThread;
    TextView text;
    Button button1,button2,button3;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 顯示文本
        text = (TextView) findViewById(R.id.text1);

        // 創建與主線程關聯的Handler
        mainHandler = new Handler();

        /**
          * 步驟1:創建HandlerThread實例對象
          * 傳入參數 = 線程名字,作用 = 標記該線程
          */
        mHandlerThread = new HandlerThread("handlerThread");

        /**
         * 步驟2:啓動線程
         */
        mHandlerThread.start();

        /**
         * 步驟3:創建工作線程Handler & 複寫handleMessage()
         * 作用:關聯HandlerThread的Looper對象、實現消息處理操作 & 與其他線程進行通信
         * 注:消息處理操作(HandlerMessage())的執行線程 = mHandlerThread所創建的工作線程中執行
         */

        workHandler = new Handler(mHandlerThread.getLooper()){
            @Override
            // 消息處理的操作
            public void handleMessage(Message msg)
            {
                //設置了兩種消息處理操作,通過msg來進行識別
                switch(msg.what){
                    // 消息1
                    case 1:
                        try {
                            //延時操作
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 通過主線程Handler.post方法進行在主線程的UI更新操作
                        mainHandler.post(new Runnable() {
                            @Override
                            public void run () {
                                text.setText("我愛學習");
                            }
                        });
                        break;

                    // 消息2
                    case 2:
                        try {
                            Thread.sleep(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        mainHandler.post(new Runnable() {
                            @Override
                            public void run () {
                                text.setText("我不喜歡學習");
                            }
                        });
                        break;
                    default:
                        break;
                }
            }
        };

        /**
         * 步驟4:使用工作線程Handler向工作線程的消息隊列發送消息
         * 在工作線程中,當消息循環時取出對應消息 & 在工作線程執行相關操作
         */
        // 點擊Button1
        button1 = (Button) findViewById(R.id.button1);
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                // 通過sendMessage()發送
                // a. 定義要發送的消息
                Message msg = Message.obtain();
                msg.what = 1; //消息的標識
                msg.obj = "A"; // 消息的存放
                // b. 通過Handler發送消息到其綁定的消息隊列
                workHandler.sendMessage(msg);
            }
        });

        // 點擊Button2
        button2 = (Button) findViewById(R.id.button2);
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                // 通過sendMessage()發送
                // a. 定義要發送的消息
                Message msg = Message.obtain();
                msg.what = 2; //消息的標識
                msg.obj = "B"; // 消息的存放
                // b. 通過Handler發送消息到其綁定的消息隊列
                workHandler.sendMessage(msg);
            }
        });

        // 點擊Button3
        // 作用:退出消息循環
        button3 = (Button) findViewById(R.id.button3);
        button3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mHandlerThread.quit();
            }
        });

    }
    
}

(4)源碼解析
4.1)工作原理
內部原理 = Thread類 + Handler類機制
(1)通過繼承Thread類,快速創建1個帶有Looper對象的新工作線程
(2)通過封裝Handler類,快速創建Handler&與其他線程進行通信
4.2)源碼解析
步驟1:創建HandlerThread的實例對象
1、具體使用

HandlerThread mHandlerThread = new HandlerThread("handlerThread");

2、源碼解析

 public class HandlerThread extends Thread {
    // 繼承自Thread類
        
        int mPriority; // 線程優先級
        int mTid = -1; // 當前線程id
        Looper mLooper; // 當前線程持有的Looper對象

       // HandlerThread類有2個構造方法
       // 區別在於:設置當前線程的優先級參數,即可自定義設置 or 使用默認優先級

            // 方式1. 默認優先級
            public HandlerThread(String name) {
                // 通過調用父類默認的方法創建線程
                super(name);
                mPriority = Process.THREAD_PRIORITY_DEFAULT;
            }
          
            // 方法2. 自定義設置優先級
            public HandlerThread(String name, int priority) {
                super(name);
                mPriority = priority;
            }
            ...
     }

3、總結
HandlerThread類繼承自Thread類
創建HandlerThread類對象 = 創建Thread類對象 + 設置線程優先級 = 新開1個工作線程 + 設置線程優先級
步驟2:啓動線程
1、具體使用

mHandlerThread.start();

2、源碼解析

/**
  * 源碼分析:此處調用的是父類(Thread類)的start(),最終回調HandlerThread的run()
  */ 
  @Override
    public void run() {
        // 1. 獲得當前線程的id
        mTid = Process.myTid();

        // 2. 創建1個Looper對象 & MessageQueue對象
        Looper.prepare();

        // 3. 通過持有鎖機制來獲得當前線程的Looper對象
        synchronized (this) {
            mLooper = Looper.myLooper();
           
            // 發出通知:當前線程已經創建mLooper對象成功
            // 此處主要是通知getLooper()中的wait()
            notifyAll();
            
            // 此處使用持有鎖機制 + notifyAll() 是爲了保證後面獲得Looper對象前就已創建好Looper對象
        }

        // 4. 設置當前線程的優先級
        Process.setThreadPriority(mPriority);

        // 5. 在線程循環前做一些準備工作 ->>分析1
        // 該方法實現體是空的,子類可實現 / 不實現該方法
        onLooperPrepared();

        // 6. 進行消息循環,即不斷從MessageQueue中取消息 & 派發消息
        Looper.loop();

        mTid = -1;
    }
}

/**
  * 分析1:onLooperPrepared();
  * 說明:該方法實現體是空的,子類可實現 / 不實現該方法
  */ 
    protected void onLooperPrepared() {

    }

3、總結
1、爲當前工作線程(即步驟1創建的線程)創建1個Looper對象 & MessageQueue對象
2、通過持有鎖機制來獲得當前線程的Looper對象
3、發出通知:當前線程已經創建mLooper對象成功
4、工作線程進行消息循環,即不斷從MessageQueue中取消息 & 派發消息
步驟3:創建工作線程Handler & 複寫handleMessage()
1、具體使用

Handler workHandler = new Handler( handlerThread.getLooper() ) {
            @Override
            public boolean handleMessage(Message msg) {
                ...//消息處理
                return true;
            }
        });

2、源碼解析

/**
  * 源碼分析:handlerThread.getLooper()
  * 作用:獲得當前HandlerThread線程中的Looper對象
  */ 
    public Looper getLooper() {
        // 若線程不是存活的,則直接返回null
        if (!isAlive()) {
            return null;
        } 
        // 若當前線程存活,再判斷線程的成員變量mLooper是否爲null
        // 直到線程創建完Looper對象後才能獲得Looper對象,若Looper對象未創建成功,則阻塞
        synchronized (this) {
  
      
            while (isAlive() && mLooper == null) {
                try {
                    // 此處會調用wait方法去等待
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        // 上述步驟run()使用 持有鎖機制 + notifyAll()  獲得Looper對象後
        // 則通知當前線程的wait()結束等待 & 跳出循環
        // 最終getLooper()返回的是在run()中創建的mLooper對象
        return mLooper;
    }

3、總結
在獲得HandlerThread工作線程的Looper對象時存在一個同步的問題:只有當線程創建成功 & 其對應的Looper對象也創建成功後才能獲得Looper的值,才能將創建的Handler 與 工作線程的Looper對象綁定,從而將Handler綁定工作線程
解決方案:即保證同步的解決方案 = 同步鎖、wait() 和 notifyAll(),即 在run()中成功創建Looper對象後,立即調用notifyAll()通知 getLooper()中的wait()結束等待 & 返回run()中成功創建的Looper對象,使得Handler與該Looper對象綁定
步驟4:使用工作線程Handler向工作線程的消息隊列發送消息
1、具體使用

 // a. 定義要發送的消息
  Message msg = Message.obtain();
  msg.what = 2; //消息的標識
  msg.obj = "B"; // 消息的存放
  // b. 通過Handler發送消息到其綁定的消息隊列
  workHandler.sendMessage(msg);

2、源碼解析
源碼分析:workHandler.sendMessage(msg)
此處的源碼即Handler的源碼,故不作過多描述
步驟5:結束線程,即停止線程的消息循環
1、具體使用

  mHandlerThread.quit();

2、源碼解析

/**
  * 源碼分析:mHandlerThread.quit()
  * 說明:
  *     a. 該方法屬於HandlerThread類
  *     b. HandlerThread有2種讓當前線程退出消息循環的方法:quit() 、quitSafely()
  */ 
    
  // 方式1:quit() 
  // 特點:效率高,但線程不安全
  public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quit(); 
            return true;
        }
        return false;
    }

  // 方式2:quitSafely()
  // 特點:效率低,但線程安全
  public boolean quitSafely() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quitSafely();
            return true;
        }
        return false;
    }

  // 注:上述2個方法最終都會調用MessageQueue.quit(boolean safe)->>分析1

/**
  * 分析1:MessageQueue.quit(boolean safe)
  */ 
    void quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }
        synchronized (this) {
            if (mQuitting) {
                return;
            }
            mQuitting = true;

            
            if (safe) {
                removeAllFutureMessagesLocked(); // 方式1(不安全)會調用該方法 ->>分析2
            } else {
                removeAllMessagesLocked(); // 方式2(安全)會調用該方法 ->>分析3
            }
            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr);
        }
    }
/**
  * 分析2:removeAllMessagesLocked()
  * 原理:遍歷Message鏈表、移除所有信息的回調 & 重置爲null
  */ 
  private void removeAllMessagesLocked() {
    Message p = mMessages;
    while (p != null) {
        Message n = p.next;
        p.recycleUnchecked();
        p = n;
    }
    mMessages = null;
}
/**
  * 分析3:removeAllFutureMessagesLocked() 
  * 原理:先判斷當前消息隊列是否正在處理消息
  *      a. 若不是,則類似分析2移除消息
  *      b. 若是,則等待該消息處理處理完畢再使用分析2中的方式移除消息退出循環
  * 結論:退出方法安全與否(quitSafe() 或 quit()),在於該方法移除消息、退出循環時是否在意當前隊列是否正在處理消息
  */ 
  private void removeAllFutureMessagesLocked() {

    final long now = SystemClock.uptimeMillis();
    Message p = mMessages;

    if (p != null) {
        // 判斷當前消息隊列是否正在處理消息
        // a. 若不是,則直接移除所有回調
        if (p.when > now) {
            removeAllMessagesLocked();
        } else {
        // b. 若是正在處理,則等待該消息處理處理完畢再退出該循環
            Message n;
            for (;;) {
                n = p.next;
                if (n == null) {
                    return;
                }
                if (n.when > now) {
                    break;
                }
                p = n;
            }
            p.next = null;
            do {
                p = n;
                n = p.next;
                p.recycleUnchecked();
            } while (n != null);
        }
    }
}

4.3)總結
在這裏插入圖片描述
(5)問題&解決
5.1)內存泄露
1、問題

In Android, Handler classes should be static or leaks might occur.

2、原因
Handler導致內存泄露:當Handler消息隊列 還有未處理的消息 / 正在處理消息時,存在引用關係: “未被處理 / 正處理的消息 -> Handler實例 -> 外部類”
若出現 Handler的生命週期 > 外部類的生命週期 時(即 Handler消息隊列 還有未處理的消息 / 正在處理消息 而 外部類需銷燬時),將使得外部類無法被垃圾回收器(GC)回收,從而造成 內存泄露
3、解決
將Handler子類設置爲靜態內部類+使用weakReference弱引用持有Activity實例
5.2)連續發送消息
1、問題
當你連續點擊3下時,發現並無按照最新點擊的按鈕操作顯示,而是按順序的一個個顯示出來
2、原因
使用HandlerThread時只是開了一個工作線程,當你點擊了n下後,只是將n個消息發送到消息隊列MessageQueue裏排隊,等候派發消息給Handler再進行對應的操作

3、IntentService

(1)介紹
Android裏的一個封裝類,繼承四大組件之一Service,用於處理異步請求&實現多線程。線程任務需按順序、在後臺執行。適用於離線下載,不符合多個數據同時請求的場景(所有任務都在同一個Thread looper裏執行)
(2)使用
步驟1:定義 IntentService的子類
傳入線程名稱、複寫onHandleIntent()方法

public class myIntentService extends IntentService {

  /** 
    * 在構造函數中傳入線程名字
    **/  
    public myIntentService() {
        // 調用父類的構造函數
        // 參數 = 工作線程的名字
        super("myIntentService");
    }

   /** 
     * 複寫onHandleIntent()方法
     * 根據 Intent實現 耗時任務 操作
     **/  
    @Override
    protected void onHandleIntent(Intent intent) {

        // 根據 Intent的不同,進行不同的事務處理
        String taskName = intent.getExtras().getString("taskName");
        switch (taskName) {
            case "task1":
                Log.i("myIntentService", "do task1");
                break;
            case "task2":
                Log.i("myIntentService", "do task2");
                break;
            default:
                break;
        }
    }

    @Override
    public void onCreate() {
        Log.i("myIntentService", "onCreate");
        super.onCreate();
    }
   /** 
     * 複寫onStartCommand()方法
     * 默認實現 = 將請求的Intent添加到工作隊列裏
     **/  
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("myIntentService", "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        Log.i("myIntentService", "onDestroy");
        super.onDestroy();
    }
}

步驟2:在Manifest.xml中註冊服務

<service android:name=".myIntentService">
            <intent-filter >
                <action android:name="cn.scu.finch"/>
            </intent-filter>
        </service>

步驟3:在Activity中開啓Service服務

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

            // 同一服務只會開啓1個工作線程
            // 在onHandleIntent()函數裏,依次處理傳入的Intent請求
            // 將請求通過Bundle對象傳入到Intent,再傳入到服務裏

            // 請求1
            Intent i = new Intent("cn.scu.finch");
            Bundle bundle = new Bundle();
            bundle.putString("taskName", "task1");
            i.putExtras(bundle);
            startService(i);

            // 請求2
            Intent i2 = new Intent("cn.scu.finch");
            Bundle bundle2 = new Bundle();
            bundle2.putString("taskName", "task2");
            i2.putExtras(bundle2);
            startService(i2);

            startService(i);  //多次啓動
        }
    }

(3)源碼解析
3.1)工作原理
在這裏插入圖片描述
若啓動IntentService 多次,那麼 每個耗時操作 則 以隊列的方式 在 IntentService的 onHandleIntent回調方法中依次執行,執行完自動結束
3.2)源碼解析
問題1:IntentService如何單獨開啓1個新的工作線程
IntentService源碼中的 onCreate()方法

@Override
public void onCreate() {
    super.onCreate();
    
    // 1. 通過實例化andlerThread新建線程 & 啓動;故 使用IntentService時,不需額外新建線程
    // HandlerThread繼承自Thread,內部封裝了 Looper
    HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
    thread.start();
  
    // 2. 獲得工作線程的 Looper & 維護自己的工作隊列
    mServiceLooper = thread.getLooper();

    // 3. 新建mServiceHandler & 綁定上述獲得Looper
    // 新建的Handler 屬於工作線程 ->>分析1
    mServiceHandler = new ServiceHandler(mServiceLooper); 
}


   /** 
     * 分析1:ServiceHandler源碼分析
     **/ 
     private final class ServiceHandler extends Handler {

         // 構造函數
         public ServiceHandler(Looper looper) {
         super(looper);
       }

        // IntentService的handleMessage()把接收的消息交給onHandleIntent()處理
        @Override
         public void handleMessage(Message msg) {
  
          // onHandleIntent 方法在工作線程中執行
          // onHandleIntent() = 抽象方法,使用時需重寫 ->>分析2
          onHandleIntent((Intent)msg.obj);
          // 執行完調用 stopSelf() 結束服務
          stopSelf(msg.arg1);

    }
}

   /** 
     * 分析2: onHandleIntent()源碼分析
     * onHandleIntent() = 抽象方法,使用時需重寫
     **/ 
      @WorkerThread
      protected abstract void onHandleIntent(Intent intent);

問題2:IntentService 如何通過onStartCommand() 將Intent 傳遞給服務 & 依次插入到工作隊列中

/** 
  * onStartCommand()源碼分析
  * onHandleIntent() = 抽象方法,使用時需重寫
  **/ 
  public int onStartCommand(Intent intent, int flags, int startId) {

    // 調用onStart()->>分析1
    onStart(intent, startId);
    return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

/** 
  * 分析1:onStart(intent, startId)
  **/ 
  public void onStart(Intent intent, int startId) {

    // 1. 獲得ServiceHandler消息的引用
    Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;

    // 2. 把 Intent參數 包裝到 message 的 obj 發送消息中,
    //這裏的Intent  = 啓動服務時startService(Intent) 裏傳入的 Intent
    msg.obj = intent;

    // 3. 發送消息,即 添加到消息隊列裏
    mServiceHandler.sendMessage(msg);
}

3.3)源碼總結
IntentService本質 = Handler + HandlerThread:
1、通過HandlerThread 單獨開啓1個工作線程:IntentService
2、創建1個內部 Handler :ServiceHandler
3、綁定 ServiceHandler 與 IntentService
4、通過 onStartCommand() 傳遞服務intent 到ServiceHandler 、依次插入Intent到工作隊列中 & 逐個發送給 onHandleIntent()
5、通過onHandleIntent() 依次處理所有Intent對象所對應的任務
因此我們通過複寫onHandleIntent() & 在裏面 根據Intent的不同進行不同線程操作 即可
3.4)注意事項
注意事項1:工作任務隊列 = 順序執行
即 若一個任務正在IntentService中執行,此時你再發送1個新的任務請求,這個新的任務會一直等待直到前面一個任務執行完畢後纔開始執行
原因:
1、由於onCreate()只會調用一次 = 只會創建1個工作線程;
2、當多次調用 startService(Intent)時(即 onStartCommand()也會調用多次),其實不會創建新的工作線程,只是把消息加入消息隊列中 & 等待執行。
3、所以,多次啓動 IntentService 會按順序執行事件
若服務停止,則會清除消息隊列中的消息,後續的事件不執行
注意事項2:不建議通過 bindService() 啓動 IntentService
原因:

// 在IntentService中,onBind()`默認返回null
@Override
public IBinder onBind(Intent intent) {
    return null;
}

採用 bindService()啓動 IntentService的生命週期如下:

onCreate() ->> onBind() ->> onunbind()->> onDestory()

即,並不會調用onStart() 或 onStartcommand(),故不會將消息發送到消息隊列,那麼onHandleIntent()將不會回調,即無法實現多線程的操作
此時,你應該使用Service,而不是IntentService
(4)對比
4.1)與Service對比
在這裏插入圖片描述
4.2)與其他線程對比
在這裏插入圖片描述

2.3)高級使用

線程池(ThreadPool)

(1)介紹
線程池是一塊緩存了一定線程數量的區域,用於複用線程和管理線程(如1、統一分配、調優&監控2、控制線程池的最大併發數)
降低因線程創建&銷燬帶來的性能開銷(重用緩存在線程池的線程)
提高線程響應速度&執行效率:1、重用線程 = 不需創建線程,即可馬上執行2、管理線程 = 優化線程執行順序(避免大量線程間因互相搶佔系統資源而到只阻塞現象)
提高對線程的管理度
注:傳統多線程方式(集成Thread類 & 實現Runnable接口)的問題
1、每次新建/銷燬線程對象消耗資源、響應速度慢
2、線程缺乏統一管理,容易出現阻塞情況
(2)工作原理
2.1核心參數
在這裏插入圖片描述
上述6個參數的配置 決定了 線程池的功能,具體設置時機 = 創建 線程池類對象時 傳入
ThreadPoolExecutor類 = 線程池的真正實現類
開發者可根據不同需求 配置核心參數,從而實現自定義線程池

// 創建線程池對象如下
// 通過 構造方法 配置核心參數
   Executor executor = new ThreadPoolExecutor( 
                                              CORE_POOL_SIZE,
                                              MAXIMUM_POOL_SIZE,
                                              KEEP_ALIVE,
                                              TimeUnit.SECONDS, 
                                              sPoolWorkQueue,
                                              sThreadFactory 
                                               );

// 構造函數源碼分析
    public ThreadPoolExecutor (int corePoolSize,
                               int maximumPoolSize,
                               long keepAliveTime,
                               TimeUnit unit,
                               BlockingQueue<Runnable workQueue>,
                               ThreadFactory threadFactory )

2.2內部原理邏輯
在這裏插入圖片描述
(3)使用流程

// 1. 創建線程池
   // 創建時,通過配置線程池的參數,從而實現自己所需的線程池
   Executor threadPool = new ThreadPoolExecutor(
                                              CORE_POOL_SIZE,
                                              MAXIMUM_POOL_SIZE,
                                              KEEP_ALIVE,
                                              TimeUnit.SECONDS,
                                              sPoolWorkQueue,
                                              sThreadFactory
                                              );
    // 注:在Java中,已內置4種常見線程池,下面會詳細說明

// 2. 向線程池提交任務:execute()
    // 說明:傳入 Runnable對象
       threadPool.execute(new Runnable() {
            @Override
            public void run() {
                ... // 線程執行任務
            }
        });

// 3. 關閉線程池shutdown() 
  threadPool.shutdown();
  
  // 關閉線程的原理
  // a. 遍歷線程池中的所有工作線程
  // b. 逐個調用線程的interrupt()中斷線程(注:無法響應中斷的任務可能永遠無法終止)

  // 也可調用shutdownNow()關閉線程:threadPool.shutdownNow()
  // 二者區別:
  // shutdown:設置 線程池的狀態 爲 SHUTDOWN,然後中斷所有沒有正在執行任務的線程
  // shutdownNow:設置 線程池的狀態 爲 STOP,然後嘗試停止所有的正在執行或暫停任務的線程,並返回等待執行任務的列表
  // 使用建議:一般調用shutdown()關閉線程池;若任務不一定要執行完,則調用shutdownNow()

作者:Carson_Ho
鏈接:https://www.jianshu.com/p/0e4a5e70bf0e
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

(4)常見的4類功能線程池
4.1定長線程池(FixedThreadPool)
1、特點
只有核心線程 & 不會被回收、線程數量固定、任務隊列無大小限制(超出的線程任務會在隊列中等待)
2、應用場景
控制線程最大併發數
3、具體使用
通過 Executors.newFixedThreadPool() 創建

// 1. 創建定長線程池對象 & 設置線程池線程數量固定爲3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
  public void run(){
    System.out.println("執行任務啦");
     }
    };
        
// 3. 向線程池提交任務:execute()
fixedThreadPool.execute(task);
        
// 4. 關閉線程池
fixedThreadPool.shutdown();

4.2定時線程池(ScheduledThreadPool)
1、特點
核心線程數量固定、非核心線程數量無限制(閒置時馬上回收)
2、應用場景
執行定時 / 週期性 任務
3、具體使用
通過Executors.newScheduledThreadPool()創建

// 1. 創建 定時線程池對象 & 設置線程池線程數量固定爲5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
       public void run(){
              System.out.println("執行任務啦");
          }
    };
// 3. 向線程池提交任務:schedule()
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延遲1s後執行任務
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延遲10ms後、每隔1000ms執行任務

// 4. 關閉線程池
scheduledThreadPool.shutdown();

4.3可緩存線程池(CachedThreadPool)
1、特點
只有非核心線程、線程數量不固定(可無限大)、靈活回收空閒線程(具備超時機制,全部回收時幾乎不佔系統資源)、新建線程(無線程可用時)
任何線程任務到來都會立刻執行,不需要等待
2、應用場景
執行大量、耗時少的線程任務
3、具體使用
通過Executors.newCachedThreadPool()創建

// 1. 創建可緩存線程池對象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
  public void run(){
        System.out.println("執行任務啦");
            }
    };

// 3. 向線程池提交任務:execute()
cachedThreadPool.execute(task);

// 4. 關閉線程池
cachedThreadPool.shutdown();

//當執行第二個任務時第一個任務已經完成
//那麼會複用執行第一個任務的線程,而不用每次新建線程。

4.4單線程化線程池(SingleThreadExecutor)
1、特點
只有一個核心線程(保證所有任務按照指定順序在一個線程中執行,不需要處理線程同步的問題)
2、應用場景
不適合併發但可能引起IO阻塞性及影響UI線程響應的操作,如數據庫操作,文件操作等
3、使用
通過Executors.newSingleThreadExecutor()創建

// 1. 創建單線程化線程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
  public void run(){
        System.out.println("執行任務啦");
            }
    };

// 3. 向線程池提交任務:execute()
singleThreadExecutor.execute(task);

// 4. 關閉線程池
singleThreadExecutor.shutdown();

2.4)對比

在這裏插入圖片描述

2.5)其他

1、Synchronized

一.設計目的
(1)JDK爲何要設計鎖
即同一時刻最多隻有1個線程執行被Synchronized修飾的方法/代碼,其他線程必須等待當前線程執行完該方法/代碼塊後才能執行該方法/代碼塊
(2)應用場景
多線程編程中,有可能會出現多個線程同時訪問一個共享、可變資源(臨界資源)的情況,這種資源可能是:對象、變量、文件等。由於線程執行的過程是不可控的,所以需要採用同步機制來協同對對象可變狀態的訪問
加鎖目的:序列化訪問臨界資源,即同一時刻只能有一個線程訪問臨界資源(同步互斥訪問)
1、修飾 實例方法 / 代碼塊時,(同步)保護的是同一個對象方法的調用 & 當前實例對象
2、修飾 靜態方法 / 代碼塊時,(同步)保護的是 靜態方法的調用 & class 類對象
二.設計原理
(1)加鎖對象
1、同步實例方法,鎖是當前實例對象
2、同步類方法,鎖是當前對象
3、同步代碼塊,鎖是括號裏的對象
(2)加鎖原理
1、依賴 JVM 實現同步
2、底層通過一個監視器對象(monitor)完成, wait()、notify() 等方法也依賴於 monitor 對象
3、監視器鎖(monitor)的本質 依賴於 底層操作系統的互斥鎖(Mutex Lock)實現

(object){
//monitorenter進入同步塊
//業務邏輯
//monitorexit退出同步塊
}

(3)JVM加鎖過程
在這裏插入圖片描述
三.具體使用
Synchronized 用於 修飾 代碼塊、類的實例方法 & 靜態方法
(1)使用規則
在這裏插入圖片描述
(2)鎖的類型&等級
1、類型
Synchronized會修飾代碼塊、類的實例方法&靜態方法
在這裏插入圖片描述
2、區別
在這裏插入圖片描述
(3)使用方式

/**
 * 對象鎖
 */
    public class Test{ 
    // 對象鎖:形式1(方法鎖) 
    public synchronized void Method1(){ 
        System.out.println("我是對象鎖也是方法鎖"); 
        try{ 
            Thread.sleep(500); 
        } catch (InterruptedException e){ 
            e.printStackTrace(); 
        } 
 
    } 
 
    // 對象鎖:形式2(代碼塊形式) 
    public void Method2(){ 
        synchronized (this){ 
            System.out.println("我是對象鎖"); 
            try{ 
                Thread.sleep(500); 
            } catch (InterruptedException e){ 
                e.printStackTrace(); 
            } 
        } 
 
    } 
 }

/**
 * 方法鎖(即對象鎖中的形式1)
 */
    public synchronized void Method1(){ 
        System.out.println("我是對象鎖也是方法鎖"); 
        try{ 
            Thread.sleep(500); 
        } catch (InterruptedException e){ 
            e.printStackTrace(); 
        } 
 
    } 

/**
 * 類鎖
 */
public class Test{ 
   // 類鎖:形式1 :鎖靜態方法
    public static synchronized void Method1(){ 
        System.out.println("我是類鎖一號"); 
        try{ 
            Thread.sleep(500); 
        } catch (InterruptedException e){ 
            e.printStackTrace(); 
        } 
 
    } 
 
    // 類鎖:形式2 :鎖靜態代碼塊
    public void Method2(){ 
        synchronized (Test.class){ 
            System.out.println("我是類鎖二號"); 
            try{ 
                Thread.sleep(500); 
            } catch (InterruptedException e){ 
                e.printStackTrace(); 
            } 
 
        } 
 
    } 
}

四.特點
在這裏插入圖片描述

2、ThreadLocal

(1)簡介
ThreadLocal是線程的局部變量,用於爲每個線程提供1個特定空間(即該變量),以保存該線程所獨享的資源。適用於隔離線程&放置線程間數據資源共享的場景。
注:
a.使每個線程可獨立地改變自己空間內的資源(設置、存儲的值)而不會和其他線程資源衝突
b.1個變量只能被同一個進程讀、寫,若第2個線程同時執行1段含有1個ThreadLocal變量引用的代碼,它們也無法訪問到對方的ThreadLocal變量
(2)使用流程
2.1創建ThreadLocal變量

// 1. 直接創建對象
private ThreadLocal myThreadLocal = new ThreadLocal()

// 2. 創建泛型對象
private ThreadLocal myThreadLocal = new ThreadLocal<String>();

// 3. 創建泛型對象 & 初始化值
// 指定泛型的好處:不需要每次對使用get()方法返回的值作強制類型轉換
private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "This is the initial value";
    }
};

// 特別注意:
// 1. ThreadLocal實例 = 類中的private、static字段
// 2. 只需實例化對象一次 & 不需知道它是被哪個線程實例化
// 3. 每個線程都保持 對其線程局部變量副本 的隱式引用
// 4. 線程消失後,其線程局部實例的所有副本都會被垃圾回收(除非存在對這些副本的其他引用)
// 5. 雖然所有的線程都能訪問到這個ThreadLocal實例,但是每個線程只能訪問到自己通過調用ThreadLocal的set()設置的值
 // 即 哪怕2個不同的線程在同一個`ThreadLocal`對象上設置了不同的值,他們仍然無法訪問到對方的值

2.2訪問ThreadLocal變量

// 1. 設置值:set()
// 需要傳入一個Object類型的參數
myThreadLocal.set("初始值”);

// 2. 讀取ThreadLocal變量中的值:get()
// 返回一個Object對象
String threadLocalValue = (String) myThreadLocal.get();

(3)具體使用

 public class ThreadLocalTest {

        // 測試代碼
        public static void main(String[] args){
            // 新開2個線程用於設置 & 獲取 ThreadLoacl的值
            MyRunnable runnable = new MyRunnable();
            new Thread(runnable, "線程1").start();
            new Thread(runnable, "線程2").start();
        }

        // 線程類
        public static class MyRunnable implements Runnable {

            // 創建ThreadLocal & 初始化
            private ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
                @Override
                protected String initialValue() {
                    return "初始化值";
                }
            };

            @Override
            public void run() {

                // 運行線程時,分別設置 & 獲取 ThreadLoacl的值
                String name = Thread.currentThread().getName();
                threadLocal.set(name + "的threadLocal"); // 設置值 = 線程名
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(name + ":" + threadLocal.get());
            }
        }
    }

測試結果

線程1:線程1的threadLocal
線程2:線程2的threadLocal

從上述結果看出,在2個線程分別設置ThreadLocal值 & 分別獲取,結果並未互相干擾
(4)實現原理
4.1)核心原理
ThreadLocal類中有1個Map(稱:ThreadLocalMap):用於存儲每個線程 & 該線程設置的存儲在ThreadLocal變量的值
1、ThreadLocalMap的鍵Key = 當前ThreadLocal實例、值value = 該線程設置的存儲在ThreadLocal變量的值
2、該key是 ThreadLocal對象的弱引用;當要拋棄掉ThreadLocal對象時,垃圾收集器會忽略該 key的引用而清理掉ThreadLocal對象
4.2)源碼分析
如何設置 & 獲取 ThreadLocal變量裏的值

// ThreadLocal的源碼

public class ThreadLocal<T> {

    ...

  /** 
    * 設置ThreadLocal變量引用的值
    *  ThreadLocal變量引用 指向 ThreadLocalMap對象,即設置ThreadLocalMap的值 = 該線程設置的存儲在ThreadLocal變量的值
    *  ThreadLocalMap的鍵Key = 當前ThreadLocal實例
    *  ThreadLocalMap的值 = 該線程設置的存儲在ThreadLocal變量的值
    **/  
    public void set(T value) {
      
        // 1. 獲得當前線程
        Thread t = Thread.currentThread();

        // 2. 獲取該線程的ThreadLocalMap對象 ->>分析1
        ThreadLocalMap map = getMap(t);

        // 3. 若該線程的ThreadLocalMap對象已存在,則替換該Map裏的值;否則創建1個ThreadLocalMap對象
        if (map != null)
            map.set(this, value);// 替換
        else
            createMap(t, value);// 創建->>分析2
    }

  /** 
    * 獲取ThreadLocal變量裏的值
    * 由於ThreadLocal變量引用 指向 ThreadLocalMap對象,即獲取ThreadLocalMap對象的值 = 該線程設置的存儲在ThreadLocal變量的值
    **/ 
    public T get() {

        // 1. 獲得當前線程
        Thread t = Thread.currentThread();

        // 2. 獲取該線程的ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);

        // 3. 若該線程的ThreadLocalMap對象已存在,則直接獲取該Map裏的值;否則則通過初始化函數創建1個ThreadLocalMap對象
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value; // 直接獲取值
        }
        return setInitialValue(); // 初始化
    }

  /** 
    * 初始化ThreadLocal的值
    **/ 
    private T setInitialValue() {

        T value = initialValue();

        // 1. 獲得當前線程
        Thread t = Thread.currentThread();

        // 2. 獲取該線程的ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);

         // 3. 若該線程的ThreadLocalMap對象已存在,則直接替換該值;否則則創建
        if (map != null)
            map.set(this, value); // 替換
        else
            createMap(t, value); // 創建->>分析2
        return value;
    }


  /** 
    * 分析1:獲取當前線程的threadLocals變量引用
    **/ 
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

  /** 
    * 分析2:創建當前線程的ThreadLocalMap對象
    **/ 
    void createMap(Thread t, T firstValue) {
    // 新創建1個ThreadLocalMap對象 放入到 Thread類的threadLocals變量引用中:
        // a. ThreadLocalMap的鍵Key = 當前ThreadLocal實例
        // b. ThreadLocalMap的值 = 該線程設置的存儲在ThreadLocal變量的值
        t.threadLocals = new ThreadLocalMap(this, firstValue);
        // 即 threadLocals變量 屬於 Thread類中 ->> 分析3
    }

  
    ...
}

  /** 
    * 分析3:Thread類 源碼分析
    **/ 

    public class Thread implements Runnable {
       ...

       ThreadLocal.ThreadLocalMap threadLocals = null;
       // 即 Thread類持有threadLocals變量
       // 線程類實例化後,每個線程對象擁有獨立的threadLocals變量變量
       // threadLocals變量在 ThreadLocal對象中 通過set() 或 get()進行操作

       ...
}

(5)補充
5.1)ThreadLocal如何做到線程安全
1、每個線程擁有自己獨立的ThreadLocals變量(指向ThreadLocalMap對象 )
2、每當線程 訪問 ThreadLocals變量時,訪問的都是各自線程自己的ThreadLocalMap變量(鍵 - 值)
3、ThreadLocalMap變量的鍵 key = 唯一 = 當前ThreadLocal實例
5.2)與同步機制的區別
在這裏插入圖片描述
#(三)Java/Android 多線程開發聯繫/區別

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