點選上方 "後端架構師"關注, 星标或置頂一起成長
背景回複“大禮包”有驚喜禮包!
每日英文
be alike flower. spread beauty and happiness wherever you stay; irrespective of your surroundings.
像花兒一樣,無論身在何處,不管周遭環境如何,都依然潇灑的綻放自己的美麗,活出自己的精彩。
每日掏心話
其實我們每個人都擁有時光機器,有的能把我們帶回從前,叫做回憶;有的能帶我們邁向未來,被稱為夢想。
來自:譚嘉俊 | 責編:樂樂
連結:juejin.im/user/2400989124522446
後端架構師(id:study_tech)第 1055 次推文 圖 / 圖蟲
往日回顧:11 月全國程式員平均工資出爐
正文
/ 前言 /
本文章講解的内容是java線程,建議對着示例項目閱讀文章,本文章分析的相關的源碼基于java development kit(jdk) 13。
threaddemo位址:
github.com/tanjiajunbeyond/threaddemo
/ 概述 /
在說線程的概念之前,先說下程序的概念,程序是代碼在資料集合上的一次運作活動,它是系統進行資源配置設定和排程的基本機關。一個程序至少有一個線程,線程是程序中的實體,線程本身是不會獨立存在的,程序中的多個線程可以共享程序的資源(例如:記憶體位址、檔案i/o等),也可以獨立排程。
有以下三種方式實作線程:
使用核心線程實作
使用使用者線程實作
使用使用者線程和輕量級線程混合實作
java語言統一處理了不同硬體和作業系統平台的線程操作,一個線程是一個已經執行start()方法而且還沒結束的java.lang.thread類的執行個體,其中thread類的所有關鍵方法都是本地方法(native method)來的,這意味着這些方法沒有使用或者無法使用平台相關的手段來實作。
/ 線程狀态切換 /
java語言定義了六種線程狀态,要注意的是,在任意一個時間點,一個線程有且隻有五種線程狀态的其中一種,這五種線程狀态如下所示:
建立(new):線程建立後尚未啟動的狀态。
運作(runable):線程正在等待着cpu為它配置設定執行時間,進入就緒(ready)狀态,等到cpu配置設定執行時間後,線程才真正執行,進入正在運作(running)狀态。
無限期等待(waiting):這種狀态下的線程不會被cpu配置設定執行時間,它們需要其他線程顯示地喚醒。以下方法會讓線程進入這種狀态:
沒有設定參數timeout的object.wait()方法
沒有設定參數timeout的thread.join()方法
locksupport.park方法
限期等待(timed waiting):這種狀态下的線程不會被cpu配置設定執行時間,但是它無需其他線程顯示地喚醒,會在一定時間内由系統自動喚醒。以下方法會讓線程進入這種狀态:
thread.sleep()方法
有設定參數timeout的object.wait()方法
有設定參數timeout的thread.join()方法
locksupport.parknanos()方法
locksupport.parkuntil()方法
阻塞(block):線程被阻塞的狀态,在等待着一個排他鎖。
結束(terminated):線程已終止,并且已經結束執行的狀态。
/ 線程建立和運作 /
java語言提供了三種建立線程的方式,如下所示:
代碼如下所示:
這種方式的優點是在run方法内可以使用this擷取目前線程,無須使用thread.currentthread方法;缺點是因為java的類隻能繼承一個類,是以繼承thread類之後,就不能繼承其他類了,而且因為任務和代碼沒有分離,如果多個線程執行相同的任務時,需要多份任務代碼。
要注意的是,調用了start方法後,線程正在等待着cpu為它配置設定執行時間,進入就緒(ready)狀态,等到cpu配置設定執行時間後,線程才真正執行,進入正在運作(running)狀态。
這種方式的優點是因為java的類可以實作多個接口,是以這個類就可以繼承自己需要的類了,而且任務和代碼分離,如果多個線程執行相同的任務時,可以公用同一個任務的代碼,如果需要對它們區分,可以添加參數進行區分。
前面兩種方式都沒有傳回值,futuretask可以有傳回值。
/ wait和notify /
wait()方法、wait(long timeoutmillis)方法、wait(long timeoutmillis, int nanos)方法、notify()方法和notifyall()方法都是object類的方法。
當一個線程調用共享變量的wait系列方法時,這個線程進入等待狀态,直到使用下面兩種方式才會被喚醒:
其他線程調用該共享變量的notify系列方法(notify()方法或者notifyall()方法)。
其他線程調用該共享變量所在的線程的interrupt()方法後,該線程抛出interruptedexception異常傳回。
要注意的是,需要擷取到該共享變量的螢幕鎖才能調用wait方法,否則會抛出illegalmonitorstateexception異常,可以使用以下兩種方式獲得對象的螢幕鎖:
調用被關鍵字synchronized修飾的方法,代碼如下所示:
執行同步代碼塊,代碼如下所示:
源碼如下所示:
這個方法實際上調用了wait(long timeoutmillis)方法,參數timeoutmillis的值是0l。它的行為和調用wait(0l, 0)方法是一緻的。
在公衆号後端架構師背景回複“java”,擷取java面試題和答案。
參數timeoutmillis是等待的最大時間,也就是逾時時間,機關是毫秒。它的行為和調用wait(timeoutmillis, 0)方法是一緻的。
要注意的是,如果傳入了負數的timeoutmillis,就會抛出illegalargumentexception異常。
這個方法實際上調用了wait(long timeoutmillis)方法;參數timeoutmillis是等待的最大時間,也就是逾時時間,機關是毫秒;參數nanos是額外的時間,機關是納秒,範圍是0~999999(包括999999)。
隻有在參數nanos大于0的時候,參數timeoutmillis才會自增。
在一個線程上調用共享變量的notify方法後,會喚醒這個共享變量上調用wait系列方法後進入等待狀态的線程。要注意的是,一個共享變量可能有多個線程在等待,具體喚醒哪個等待的線程是随機的。
被喚醒的線程不能立即從wait系列方法傳回後繼續執行,它需要擷取到該共享變量的螢幕鎖才能傳回,也就是說,喚醒它的線程釋放了該共享變量的螢幕鎖,被喚醒的線程不一定能擷取到該共享變量的螢幕鎖,因為該線程還需要和其他線程去競争這個螢幕鎖,隻有競争到這個螢幕鎖後才能繼續執行。
隻有目前線程擷取到該共享變量的螢幕鎖後,才能調用該共享變量的notify系列方法,否則會抛出illegalmonitorstateexception異常。
notifyall()方法可以喚醒所有在該共享變量上因為調用wait系列方法而進入等待狀态的線程。
/ sleep()方法--讓線程睡眠 /
sleep()方法是thread類的一個靜态方法。當一個正在執行的線程調用了這個方法後,調用線程會暫時讓出指定睡眠時間的執行權,不參與cpu的排程,但是不會讓出該線程所擁有的螢幕鎖。指定的睡眠時間到了後,sleep()方法會正常傳回,線程處于就緒狀态,然後參與cpu排程,擷取到cpu資源後繼續運作。
要注意的是,如果在睡眠期間其他線程調用了該線程的interrupt()方法中斷了該線程,就會在調用sleep方法的地方抛出interruptedexception異常而傳回。
下面來看一個生産者消費者問題(producer-consumer problem)的例子:
repository類是一個存儲庫,存放産品,代碼如下所示:
以下是測試代碼,我先把生産者所在的線程睡眠(sleep)一秒,把消費者所在的線程睡眠三秒,這樣就可以制造出生産速度大于消費速度的場景,代碼如下所示:
運作上面的代碼,大約十秒後手動結束程序,結果如下所示:
然後我把生産者所在的線程睡眠三秒,把消費者所在的線程睡眠一秒,這樣就可以制造出生産速度小于消費速度的場景,代碼如下所示:
上面的結果都符合預期,我解釋一下,當發現隊列滿了後,就會調用變量queue的wait()方法,該生産者線程就會被進入等待狀态,并且釋放queue對象的螢幕鎖,讓其他生産者線程和消費者線程去競争這個螢幕鎖,打破了死鎖産生的四個條件中的請求并持有條件,避免發生死鎖,同樣的,當發現隊列空了後,也會調用變量queue的wait()方法,該消費者線程會進入等待狀态,并且釋放queue對象的螢幕鎖,讓其他消費者線程和生産者線程去競争這個螢幕鎖,打破了死鎖的四個條件中的請求并持有條件,避免發生死鎖。
在公衆号後端架構師背景回複“offer”,擷取算法面試題和答案。
上文提到的死鎖産生的四個條件會在後面詳細講解。
/ join系列方法--等待線程執行終止 /
join系列方法是thread類的一個普通方法。它可以處理一些需要等待某幾個任務完成後才能繼續往下執行的場景。
這個方法實際上調用了join(final long millis)方法,參數millis的值是0。
參數millis是等待時間,機關是毫秒。
要注意的是,如果傳入了負數的millis,就會抛出illegalargumentexception異常。
這個方法實際上調用了join(final long millis)方法;參數millis是等待時間,機關是毫秒;參數nanos是額外的時間,機關是納秒,範圍是0~999999(包括999999)。
隻有在參數nanos大于0的時候,參數millis才會自增。
我在寫深入了解volatile關鍵字這篇文章的時候,其中一個例子使用到了這個方法,代碼如下所示:
在這個示例代碼中調用join()方法目的是為了讓這十個子線程運作結束後,主線程才結束,保證這十個子線程都能全部運作結束。
/ yield()--讓出cpu執行權 /
yield()方法是thread類的一個靜态方法。當一個線程調用這個方法後,目前線程告訴線程排程器讓出cpu執行權,但是線程排程器可以無條件忽略這個請求,如果成功讓出後,線程處于就緒狀态,它會從線程就緒隊列中擷取一個線程優先級最高的線程,當然也有可能排程到剛剛讓出cpu執行權的那個線程來擷取cpu執行權。源碼如下所示:
它和sleep()方法的差別是:當線程調用sleep()方法時,它會被阻塞指定的時間,在這個期間線程排程器不會去排程其他線程,而當線程調用yield()方法時,線程隻是讓出自己剩餘的cpu時間片,線程還是處于就緒狀态,并沒有被阻塞,線程排程器在下一次排程時可能還會排程到這個線程執行。
/ 線程中斷 /
在java中,線程中斷是一種線程間的協作模式。
要注意的是,通過設定線程的中斷标志并不能立刻終止線程的執行,而是通過被中斷的線程的中斷标志自行處理。
interrupt()方法可以中斷線程,如果是在其他線程調用該線程的interrupt()方法,會通過checkaccess()方法檢查權限,這有可能抛出securityexception異常。假設有兩個線程,分别是線程a和線程b,當線程a正在運作時,線程b可以調用線程a的interrupt()方法來設定線程a的中斷标志為true并且立即傳回,前面也提到過,設定标志僅僅是設定标志而已,線程a實際上還在運作,還沒被中斷;如果線程a因為調用了wait系列方法、join()方法或者sleep()方法而被阻塞,這時候線程b調用線程a的interrupt()方法,線程a會在調用這些方法的地方抛出interruptedexception異常。源碼如下所示:
isinterrupted()方法可以用來檢測目前線程是否被中斷,如果是就傳回true,否則傳回false。源碼如下所示:
interrupted()方法是thread類的一個靜态方法。它可以用來檢測目前線程是否被中斷,如果是就傳回true,否則傳回false。它和上面提到的isinterrupted()方法的不同的是:如果發現目前線程被中斷,就會清除中斷标志,并且這個方法是靜态方法,可以直接調用thread.interrupted()使用。源碼如下所示:
isinterrupted()方法和interrupted()方法都是調用了isinterrupted(boolean clearinterrupted)方法,源碼如下所示:
參數clearinterrupted是用來判斷是否需要重置中斷标志。
從源碼可得知,interrupted()方法是通過擷取目前線程的中斷标志,而不是擷取調用interrupted()方法的執行個體對象的中斷标志。
/ 線程上下文切換 /
在多線程程式設計中,線程的個數一般大于cpu的個數,但是每一個cpu隻能被一個線程使用,為了讓使用者感覺多個線程在同時執行,cpu的資源配置設定政策采用的是時間片輪換政策,時間片輪換政策是指給每個線程配置設定一個時間片,線程會在時間片内占用cpu執行任務。
線程上下文切換是指目前線程使用完時間片後,處于就緒狀态,并且讓出cpu給其他線程占用,同時儲存目前線程的執行現場,用于再次執行時恢複執行現場。
/ 線程死鎖 /
線程死鎖是指兩個或者兩個以上的線程在執行的過程中,因為互相競争資源而導緻互相等待的現象,如果沒有外力的情況下,它們會一直互相等待,導緻無法繼續執行下去。
死鎖的産生必須具備以下四個條件:
互斥條件:指資源隻能由一個線程占用,如果其他線程要請求使用該資源,就隻能等待,直到占用資源的線程釋放該資源。
請求并持有條件:指一個線程已經占有至少一個資源,但是還想請求占用新的資源,而新的資源被其他線程占用,是以目前線程會被阻塞,但是阻塞的同時不釋放自己擷取的資源。
不可剝奪條件:指線程擷取到的資源在自己使用完畢之前不能被其他線程占用,隻有在自己使用完畢後才會釋放該資源。
環路等待條件:指發生在死鎖時,必然存在一個線程——資源的環形鍊,舉個例子:有一個線程集合{thead0, thread1, thread2, ……, threadn),其中thread0等待thread1占用的資源,thread1等待thread2占用的資源,thread2等待thread3占用的資源,……,threadn等待thread0占用的資源。
那如何避免死鎖呢?隻要打破其中一個條件就可以避免死鎖,不過基于作業系統的特性,隻有請求并持有條件和環路等待條件是可以破壞的。
/ 使用者線程和守護線程 /
java中的線程分為兩類:使用者線程(user thread)和守護線程(daemon thread)。在java虛拟機啟動的時候會調用main方法,main方法所在的線程就是一個使用者線程,同時java虛拟機還會啟動很多守護線程,例如:垃圾回收線程。
隻需要調用thread類的setdaemon(boolean on)方法,并且參數on設為true,就可以使該線程成為守護線程。
使用者線程和守護線程的差別是當最後一個使用者線程結束後,java虛拟機程序才會正常結束,而守護線程是否結束不影響java虛拟機程序的結束。
總結一下:如果我們希望在主線程結束後,子線程繼續工作,等到子線程結束後才讓java虛拟機程序結束,我們可以把線程設為使用者線程;如果我們希望在主線程結束後,java虛拟機程序也立即結束,我們可以把線程設為守護線程。
github位址:
github.com/tanjiajunbeyond
ps:歡迎在留言區留 下你的觀點,一起讨論提高。如果今天的文章讓你有新的啟發,歡迎轉發分享給更多人。
歡迎加入後端架構師交流群,在背景回複“007”即可。
猜你還想看
阿裡、騰訊、百度、華為、京東最新面試題彙集
自從上線了 prometheus 監控告警,真香!
剛剛,python 之父放棄退休,64歲的他宣布加入微軟!
一步步實作 redis 搜尋引擎
嘿,你在看嗎?