我爲了扒了併發,寫了 2 萬字 40 張圖






1html

 併發歷史 java


在計算機最先期的時候,沒有操做系統,執行程序只須要一種方式,那就是從頭至尾依次執行。任何資源都會爲這個程序服務,在計算機使用某些資源時,其餘資源就會空閒,就會存在 浪費資源 的狀況。程序員

這裏說的浪費資源指的是資源空閒,沒有充分使用的狀況。web

操做系統的出現爲咱們的程序帶來了 併發性,操做系統使咱們的程序可以同時運行多個程序,一個程序就是一個進程,也就至關於同時運行多個進程。面試

操做系統是一個併發系統,併發性是操做系統很是重要的特徵,操做系統具備同時處理和調度多個程序的能力,好比多個 I/O 設備同時在輸入輸出;設備 I/O 和 CPU 計算同時進行;內存中同時有多個系統和用戶程序被啓動交替、穿插地執行。操做系統在協調和分配進程的同時,操做系統也會爲不一樣進程分配不一樣的資源。算法

操做系統實現多個程序同時運行解決了單個程序沒法作到的問題,主要有下面三點數據庫

  • 資源利用率,咱們上面說到,單個進程存在資源浪費的狀況,舉個例子,當你在爲某個文件夾賦予權限的時候,輸入程序沒法接受外部的輸入字符,只有等到權限賦予完畢後才能接受外部輸入。總的來說,就是在等待程序時沒法執行其餘工做。若是在等待程序時能夠運行另外一個程序,那麼將會大大提升資源的利用率。(資源並不會以爲累)由於它不會划水~
  • 公平性,不一樣的用戶和程序都可以使用計算機上的資源。一種高效的運行方式是爲不一樣的程序劃分時間片來使用資源,可是有一點須要注意,操做系統能夠決定不一樣進程的優先級。雖然每一個進程都有可以公平享有資源的權利,可是當有一個進程釋放資源後的同時有一個優先級更高的進程搶奪資源,就會形成優先級低的進程沒法得到資源,進而致使進程飢餓。
  • 便利性,單個進程是是不用通訊的,通訊的本質就是 信息交換,及時進行信息交換可以避免 信息孤島,作重複性的工做;任何併發能作的事情,單進程也可以實現,只不過這種方式效率很低,它是一種 順序性的。

可是,順序編程(也稱爲串行編程)也不是一無可取的,串行編程的優點在於其「直觀性和簡單性」,客觀來說,串行編程更適合咱們人腦的思考方式,可是咱們並不會知足於順序編程,「we want it more!!!」 。資源利用率、公平性和便利性促使着進程出現的同時,也促使着線程的出現。編程

若是你還不是很理解進程和線程的區別的話,那麼我就以我多年操做系統的經驗(吹牛逼,實則半年)來爲你解釋一下:「進程是一個應用程序,而線程是應用程序中的一條順序流」數組

進程中會有多個線程來完成一些任務,這些任務有可能相同有可能不一樣。每一個線程都有本身的執行順序。
瀏覽器

每一個線程都有本身的棧空間,這是線程私有的,還有一些其餘線程內部的和線程共享的資源,以下所示。

在計算機中,通常堆棧指的就是棧,而堆指的纔是堆

線程會共享進程範圍內的資源,例如內存和文件句柄,可是每一個線程也有本身私有的內容,好比程序計數器、棧以及局部變量。下面彙總了進程和線程共享資源的區別

線程是一種輕量級的進程,輕量級體如今線程的建立和銷燬要比進程的開銷小不少。

注意:任何比較都是相對的。

在大多數現代操做系統中,都以線程爲基本的調度單位,因此咱們的視角着重放在對線程的探究。

2

 線程 


什麼是多線程

多線程意味着你可以在同一個應用程序中運行多個線程,咱們知道,指令是在 CPU 中執行的,多線程應用程序就像是具備多個 CPU 在同時執行應用程序的代碼。

其實這是一種假象,線程數量並不等於 CPU 數量,單個 CPU 將在多個線程之間共享 CPU 的時間片,在給定的時間片內執行每一個線程之間的切換,每一個線程也能夠由不一樣的 CPU 執行,以下圖所示

併發和並行的關係

併發意味着應用程序會執行多個的任務,可是若是計算機只有一個 CPU 的話,那麼應用程序沒法同時執行多個的任務,可是應用程序又須要執行多個任務,因此計算機在開始執行下一個任務以前,它並無完成當前的任務,只是把狀態暫存,進行任務切換,CPU 在多個任務之間進行切換,直到任務完成。以下圖所示

並行是指應用程序將其任務分解爲較小的子任務,這些子任務能夠並行處理,例如在多個CPU上同時進行。

優點和劣勢

合理使用線程是一門藝術,合理編寫一道準確無誤的多線程程序更是一門藝術,若是線程使用得當,可以有效的下降程序的開發和維護成本。

Java 很好的在用戶空間實現了開發工具包,並在內核空間提供系統調用來支持多線程編程,Java 支持了豐富的類庫 java.util.concurrent 和跨平臺的內存模型,同時也提升了開發人員的門檻,併發一直以來是一個高階的主題,可是如今,併發也成爲了主流開發人員的必備素質。

雖然線程帶來的好處不少,可是編寫正確的多線程(併發)程序是一件極困難的事情,併發程序的 Bug 每每會詭異地出現又詭異的消失,在當你認爲沒有問題的時候它就出現了,難以定位 是併發程序的一個特徵,因此在此基礎上你須要有紮實的併發基本功。那麼,併發爲何會出現呢?

併發爲何會出現

計算機世界的快速發展離不開 CPU、內存和 I/O 設備的高速發展,可是這三者一直存在速度差別性問題,咱們能夠從存儲器的層次結構能夠看出

CPU 內部是寄存器的構造,寄存器的訪問速度要高於高速緩存,高速緩存的訪問速度要高於內存,最慢的是磁盤訪問。

程序是在內存中執行的,程序裏大部分語句都要訪問內存,有些還須要訪問 I/O 設備,根據漏桶理論來講,程序總體的性能取決於最慢的操做也就是磁盤訪問速度。

由於 CPU 速度太快了,因此爲了發揮 CPU 的速度優點,平衡這三者的速度差別,計算機體系機構、操做系統、編譯程序都作出了貢獻,主要體現爲:

  • CPU 使用緩存來中和和內存的訪問速度差別
  • 操做系統提供進程和線程調度,讓 CPU 在執行指令的同時分時複用線程,讓內存和磁盤不斷交互,不一樣的 CPU 時間片 可以執行不一樣的任務,從而均衡這三者的差別
  • 編譯程序提供優化指令的執行順序,讓緩存可以合理的使用

咱們在享受這些便利的同時,多線程也爲咱們帶來了挑戰,下面咱們就來探討一下併發問題爲何會出現以及多線程的源頭是什麼

線程帶來的安全性問題

線程安全性是很是複雜的,在沒有采用同步機制的狀況下,多個線程中的執行操做每每是不可預測的,這也是多線程帶來的挑戰之一,下面咱們給出一段代碼,來看看安全性問題體如今哪

public class TSynchronized implements Runnable{

    static int i = 0;

    public void increase(){
        i++;
    }


    @Override
    public void run() {
        for(int i = 0;i < 1000;i++) {
            increase();
        }
    }

    public static void main(String[] args) throws InterruptedException {

        TSynchronized tSynchronized = new TSynchronized();
        Thread aThread = new Thread(tSynchronized);
        Thread bThread = new Thread(tSynchronized);
        aThread.start();
        bThread.start();
        System.out.println("i = " + i);
    }
}

這段程序輸出後會發現,i 的值每次都不同,這不符合咱們的預測,那麼爲何會出現這種狀況呢?咱們先來分析一下程序的運行過程。

TSynchronized 實現了 Runnable 接口,並定義了一個靜態變量 i,而後在 increase 方法中每次都增長 i 的值,在其實現的 run 方法中進行循環調用,共執行 1000 次。

可見性問題

在單核 CPU 時代,全部的線程共用一個 CPU,CPU 緩存和內存的一致性問題容易解決,CPU 和 內存之間

若是用圖來表示的話我想會是下面這樣

在多核時代,由於有多核的存在,每一個核都可以獨立的運行一個線程,每顆 CPU 都有本身的緩存,這時 CPU 緩存與內存的數據一致性就沒那麼容易解決了,當多個線程在不一樣的 CPU 上執行時,這些線程操做的是不一樣的 CPU 緩存

由於 i 是靜態變量,沒有通過任何線程安全措施的保護,多個線程會併發修改 i 的值,因此咱們認爲 i 不是線程安全的,致使這種結果的出現是因爲 aThread 和 bThread 中讀取的 i 值彼此不可見,因此這是因爲 可見性 致使的線程安全問題。

原子性問題

看起來很普通的一段程序卻由於兩個線程 aThreadbThread 交替執行產生了不一樣的結果。可是根源不是由於建立了兩個線程致使的,多線程只是產生線程安全性的必要條件,最終的根源出如今 i++ 這個操做上。

這個操做怎麼了?這不就是一個給 i 遞增的操做嗎?也就是 「i++ => i = i + 1」,這怎麼就會產生問題了?

由於 i++ 不是一個 原子性 操做,仔細想一下,i++ 其實有三個步驟,讀取 i 的值,執行 i + 1 操做,而後把 i + 1 得出的值從新賦給 i(將結果寫入內存)。

當兩個線程開始運行後,每一個線程都會把 i 的值讀入到 CPU 緩存中,而後執行 + 1 操做,再把 + 1 以後的值寫入內存。由於線程間都有各自的虛擬機棧和程序計數器,他們彼此之間沒有數據交換,因此當 aThread 執行 + 1 操做後,會把數據寫入到內存,同時 bThread 執行 + 1 操做後,也會把數據寫入到內存,由於 CPU 時間片的執行週期是不肯定的,因此會出現當 aThread 尚未把數據寫入內存時,bThread 就會讀取內存中的數據,而後執行 + 1操做,再寫回內存,從而覆蓋 i 的值,致使 aThread 所作的努力白費。

爲何上面的線程切換會出現問題呢?

咱們先來考慮一下正常狀況下(即不會出現線程安全性問題的狀況下)兩條線程的執行順序

能夠看到,當 aThread 在執行完整個 i++ 的操做後,操做系統對線程進行切換,由 aThread -> bThread,這是最理想的操做,一旦操做系統在任意 讀取/增長/寫入 階段產生線程切換,都會產生線程安全問題。例如以下圖所示

最開始的時候,內存中 i = 0,aThread 讀取內存中的值並把它讀取到本身的寄存器中,執行 +1 操做,此時發生線程切換,bThread 開始執行,讀取內存中的值並把它讀取到本身的寄存器中,此時發生線程切換,線程切換至 aThread 開始運行,aThread 把本身寄存器的值寫回到內存中,此時又發生線程切換,由 aThread -> bThread,線程 bThread 把本身寄存器的值 +1 而後寫回內存,寫完後內存中的值不是 2 ,而是 1, 內存中的 i 值被覆蓋了。

咱們上面提到 原子性 這個概念,那麼什麼是原子性呢?

併發編程的原子性操做是徹底獨立於任何其餘進程運行的操做,原子操做多用於現代操做系統和並行處理系統中。

原子操做一般在內核中使用,由於內核是操做系統的主要組件。可是,大多數計算機硬件,編譯器和庫也提供原子性操做。

在加載和存儲中,計算機硬件對存儲器字進行讀取和寫入。爲了對值進行匹配、增長或者減少操做,通常經過原子操做進行。在原子操做期間,處理器能夠在同一數據傳輸期間完成讀取和寫入。這樣,其餘輸入/輸出機制或處理器沒法執行存儲器讀取或寫入任務,直到原子操做完成爲止。

簡單來說,就是「原子操做要麼所有執行,要麼所有不執行」。數據庫事務的原子性也是基於這個概念演進的。

有序性問題

在併發編程中還有帶來讓人很是頭疼的 有序性 問題,有序性顧名思義就是順序性,在計算機中指的就是指令的前後執行順序。一個很是顯而易見的例子就是 JVM 中的類加載

這是一個 JVM 加載類的過程圖,也稱爲類的生命週期,類從加載到 JVM 到卸載一共會經歷五個階段 「加載、鏈接、初始化、使用、卸載」。這五個過程的執行順序是必定的,可是在鏈接階段,也會分爲三個過程,即 「驗證、準備、解析」 階段,這三個階段的執行順序不是肯定的,一般交叉進行,在一個階段的執行過程當中會激活另外一個階段。

有序性問題通常是編譯器帶來的,編譯器有的時候確實是 「好心辦壞事」,它爲了優化系統性能,每每更換指令的執行順序。

活躍性問題

多線程還會帶來活躍性問題,如何定義活躍性問題呢?活躍性問題關注的是 「某件事情是否會發生」

「若是一組線程中的每一個線程都在等待一個事件的發生,而這個事件只能由該組中正在等待的線程觸發,這種狀況會致使死鎖」

簡單一點來表述一下,就是每一個線程都在等待其餘線程釋放資源,而其餘資源也在等待每一個線程釋放資源,這樣沒有線程搶先釋放本身的資源,這種狀況會產生死鎖,全部線程都會無限的等待下去。

「死鎖的必要條件」

形成死鎖的緣由有四個,破壞其中一個便可破壞死鎖

  • 互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。若是此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程釋放。
  • 請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對本身已得到的其它資源保持佔有。
  • 不剝奪條件:指進程已得到的資源,在未使用完以前,不能被剝奪,只能在使用完時由本身釋放。
  • 循環等待:指在發生死鎖時,必然存在一個進程對應的環形鏈。

換句話說,死鎖線程集合中的每一個線程都在等待另外一個死鎖線程佔有的資源。可是因爲全部線程都不能運行,它們之中任何一個資源都沒法釋放資源,因此沒有一個線程能夠被喚醒。

若是說死鎖很癡情的話,那麼活鎖用一則成語來表示就是 弄巧成拙

某些狀況下,當線程意識到它不能獲取所須要的下一個鎖時,就會嘗試禮貌的釋放已經得到的鎖,而後等待很是短的時間再次嘗試獲取。能夠想像一下這個場景:當兩我的在狹路相逢的時候,都想給對方讓路,相同的步調會致使雙方都沒法前進。

如今假想有一對並行的線程用到了兩個資源。它們分別嘗試獲取另外一個鎖失敗後,兩個線程都會釋放本身持有的鎖,再次進行嘗試,這個過程會一直進行重複。很明顯,這個過程當中沒有線程阻塞,可是線程仍然不會向下執行,這種情況咱們稱之爲 活鎖(livelock)

若是咱們指望的事情一直不會發生,就會產生活躍性問題,好比單線程中的無限循環

while(true){...}

for(;;){}

在多線程中,好比 aThread 和 bThread 都須要某種資源,aThread 一直佔用資源不釋放,bThread 一直得不到執行,就會形成活躍性問題,bThread 線程會產生飢餓,咱們後面會說。

性能問題

與活躍性問題密切相關的是 性能 問題,若是說活躍性問題關注的是最終的結果,那麼性能問題關注的就是形成結果的過程,性能問題有不少方面:好比「服務時間過長,吞吐率太低,資源消耗太高」,在多線程中這樣的問題一樣存在。

在多線程中,有一個很是重要的性能因素那就是咱們上面提到的 線程切換,也稱爲 上下文切換(Context Switch),這種操做開銷很大。

在計算機世界中,老外都喜歡用 context 上下文這個詞,這個詞涵蓋的內容不少,包括上下文切換的資源,寄存器的狀態、程序計數器等。context switch 通常指的就是這些上下文切換的資源、寄存器狀態、程序計數器的變化等。

在上下文切換中,會保存和恢復上下文,丟失局部性,把大量的時間消耗在線程切換上而不是線程運行上。

爲何線程切換會開銷如此之大呢?線程間的切換會涉及到如下幾個步驟

將 CPU 從一個線程切換到另外一線程涉及掛起當前線程,保存其狀態,例如寄存器,而後恢復到要切換的線程的狀態,加載新的程序計數器,此時線程切換實際上就已經完成了;此時,CPU 不在執行線程切換代碼,進而執行新的和線程關聯的代碼。

引發線程切換的幾種方式

線程間的切換通常是操做系統層面須要考慮的問題,那麼引發線程上下文切換有哪幾種方式呢?或者說線程切換有哪幾種誘因呢?主要有下面幾種引發上下文切換的方式

  • 當前正在執行的任務完成,系統的 CPU 正常調度下一個須要運行的線程
  • 當前正在執行的任務遇到 I/O 等阻塞操做,線程調度器掛起此任務,繼續調度下一個任務。
  • 多個任務併發搶佔鎖資源,當前任務沒有得到鎖資源,被線程調度器掛起,繼續調度下一個任務。
  • 用戶的代碼掛起當前任務,好比線程執行 sleep 方法,讓出CPU。
  • 使用硬件中斷的方式引發上下文切換

3

 線程安全性



在 Java 中,要實現線程安全性,必需要正確的使用線程和鎖,可是這些只是知足線程安全的一種方式,要編寫正確無誤的線程安全的代碼,其核心就是對狀態訪問操做進行管理。最重要的就是最 共享(Shared)的 和 可變(Mutable)的狀態。只有共享和可變的變量纔會出現問題,私有變量不會出現問題,參考程序計數器

對象的狀態能夠理解爲存儲在實例變量或者靜態變量中的數據,共享意味着某個變量能夠被多個線程同時訪問、可變意味着變量在生命週期內會發生變化。一個變量是不是線程安全的,取決於它是否被多個線程訪問。要使變量可以被安全訪問,必須經過同步機制來對變量進行修飾。

若是不採用同步機制的話,那麼就要避免多線程對共享變量的訪問,主要有下面兩種方式

  • 不要在多線程之間共享變量
  • 將共享變量置爲不可變的

咱們說了這麼屢次線程安全性,那麼什麼是線程安全性呢?

什麼是線程安全性

多個線程能夠同時安全調用的代碼稱爲線程安全的,若是一段代碼是安全的,那麼這段代碼就不存在 競態條件。僅僅當多個線程共享資源時,纔會出現競態條件。

根據上面的探討,咱們能夠得出一個簡單的結論:「當多個線程訪問某個類時,這個類始終都能表現出正確的行爲,那麼就稱這個類是線程安全的」

單線程就是一個線程數量爲 1 的多線程,單線程必定是線程安全的。讀取某個變量的值不會產生安全性問題,由於無論讀取多少次,這個變量的值都不會被修改。

原子性

咱們上面提到了原子性的概念,你能夠把原子性操做想象成爲一個不可分割 的總體,它的結果只有兩種,要麼所有執行,要麼所有回滾。你能夠把原子性認爲是 婚姻關係 的一種,男人和女人只會產生兩種結果,好好的說散就散,通常男人的一輩子均可以把他當作是原子性的一種,固然咱們不排除時間管理(線程切換)的個例,咱們知道線程切換必然會伴隨着安全性問題,男人要出去浪也會形成兩種結果,這兩種結果分別對應安全性的兩個結果:線程安全(好好的)和線程不安全(說散就散)。

競態條件

有了上面的線程切換的功底,那麼競態條件也就好定義了,它指的就是「兩個或多個線程同時對一共享數據進行修改,從而影響程序運行的正確性時,這種就被稱爲競態條件(race condition)」 ,線程切換是致使競態條件出現的誘導因素,咱們經過一個示例來講明,來看一段代碼

public class RaceCondition {
  
  private Signleton single = null;
  public Signleton newSingleton(){
    if(single == null){
      single = new Signleton();
    }
    return single;
  }
  
}

在上面的代碼中,涉及到一個競態條件,那就是判斷 single 的時候,若是 single 判斷爲空,此時發生了線程切換,另一個線程執行,判斷 single 的時候,也是空,執行 new 操做,而後線程切換回以前的線程,再執行 new 操做,那麼內存中就會有兩個 Singleton 對象。

加鎖機制

在 Java 中,有不少種方式來對共享和可變的資源進行加鎖和保護。Java 提供一種內置的機制對資源進行保護:synchronized 關鍵字,它有三種保護機制

  • 對方法進行加鎖,確保多個線程中只有一個線程執行方法;
  • 對某個對象實例(在咱們上面的探討中,變量可使用對象來替換)進行加鎖,確保多個線程中只有一個線程對對象實例進行訪問;
  • 對類對象進行加鎖,確保多個線程只有一個線程可以訪問類中的資源。

synchronized 關鍵字對資源進行保護的代碼塊俗稱 同步代碼塊(Synchronized Block),例如

synchronized(lock){
  // 線程安全的代碼
}

每一個 Java 對象均可以用作一個實現同步的鎖,這些鎖被稱爲 內置鎖(Instrinsic Lock)或者 監視器鎖(Monitor Lock)。線程在進入同步代碼以前會自動得到鎖,而且在退出同步代碼時自動釋放鎖,而不管是經過正常執行路徑退出仍是經過異常路徑退出,得到內置鎖的惟一途徑就是進入這個由鎖保護的同步代碼塊或方法。

synchronized 的另外一種隱含的語義就是 互斥,互斥意味着獨佔,最多隻有一個線程持有鎖,當線程 A 嘗試得到一個由線程 B 持有的鎖時,線程 A 必須等待或者阻塞,直到線程 B 釋放這個鎖,若是線程 B 不釋放鎖的話,那麼線程 A 將會一直等待下去。

線程 A 得到線程 B 持有的鎖時,線程 A 必須等待或者阻塞,可是獲取鎖的線程 B 能夠重入,重入的意思能夠用一段代碼表示

public class Retreent {
  
  public synchronized void doSomething(){
    doSomethingElse();
    System.out.println("doSomething......");
  }
  
  public synchronized void doSomethingElse(){
    System.out.println("doSomethingElse......");
}

獲取 doSomething() 方法鎖的線程能夠執行 doSomethingElse() 方法,執行完畢後能夠從新執行 doSomething() 方法中的內容。鎖重入也支持子類和父類之間的重入,具體的咱們後面會進行介紹。

volatile 是一種輕量級的 synchronized,也就是一種輕量級的加鎖方式,volatile 經過保證共享變量的可見性來從側面對對象進行加鎖。可見性的意思就是當一個線程修改一個共享變量時,另一個線程可以 看見 這個修改的值。volatile 的執行成本要比 synchronized 低不少,由於 volatile 不會引發線程的上下文切換。

咱們還可使用原子類 來保證線程安全,原子類其實就是 rt.jar 下面以 atomic 開頭的類

除此以外,咱們還可使用 java.util.concurrent 工具包下的線程安全的集合類來確保線程安全,具體的實現類和其原理咱們後面會說。

可使用不一樣的併發模型來實現併發系統,併發模型說的是系統中的線程如何協做完成併發任務。不一樣的併發模型以不一樣的方式拆分任務,線程能夠以不一樣的方式進行通訊和協做。


4

 競態條件和關鍵區域 


競態條件是在關鍵代碼區域發生的一種特殊條件。關鍵區域是由多個線程同時執行的代碼部分,關鍵區域中的代碼執行順序會對形成不同的結果。若是多個線程執行一段關鍵代碼,而這段關鍵代碼會由於執行順序不一樣而形成不一樣的結果時,那麼這段代碼就會包含競爭條件。


5

 併發模型和分佈式系統很類似


併發模型其實和分佈式系統模型很是類似,在併發模型中是線程彼此進行通訊,而在分佈式系統模型中是 進程 彼此進行通訊。然而本質上,進程和線程也很是類似。這也就是爲何併發模型和分佈式模型很是類似的緣由。

分佈式系統一般要比並發系統面臨更多的挑戰和問題好比進程通訊、網絡可能出現異常,或者遠程機器掛掉等等。可是一個併發模型一樣面臨着好比 CPU 故障、網卡出現問題、硬盤出現問題等。

由於併發模型和分佈式模型很類似,所以他們能夠相互借鑑,例如用於線程分配的模型就相似於分佈式系統環境中的負載均衡模型。

其實說白了,分佈式模型的思想就是借鑑併發模型的基礎上推演發展來的。


6

 認識兩個狀態 


併發模型的一個重要的方面是,線程是否應該共享狀態,是具備共享狀態仍是獨立狀態。共享狀態也就意味着在不一樣線程之間共享某些狀態

狀態其實就是數據,好比一個或者多個對象。當線程要共享數據時,就會形成 競態條件 或者 死鎖 等問題。固然,這些問題只是可能會出現,具體實現方式取決於你是否安全的使用和訪問共享對象。

獨立的狀態代表狀態不會在多個線程之間共享,若是線程之間須要通訊的話,他們能夠訪問不可變的對象來實現,這是最有效的避免併發問題的一種方式,以下圖所示

使用獨立狀態讓咱們的設計更加簡單,由於只有一個線程可以訪問對象,即便交換對象,也是不可變的對象。


7

 併發模型 


並行 Worker

第一個併發模型是並行 worker 模型,客戶端會把任務交給 代理人(Delegator),而後由代理人把工做分配給不一樣的 工人(worker)。以下圖所示

並行 worker 的核心思想是,它主要有兩個進程即代理人和工人,Delegator 負責接收來自客戶端的任務並把任務下發,交給具體的 Worker 進行處理,Worker 處理完成後把結果返回給 Delegator,在 Delegator 接收到 Worker 處理的結果後對其進行彙總,而後交給客戶端。

並行 Worker 模型是 Java 併發模型中很是常見的一種模型。許多 java.util.concurrent 包下的併發工具都使用了這種模型。

並行 Worker 的優勢

並行 Worker 模型的一個很是明顯的特色就是很容易理解,爲了提升系統的並行度你能夠增長多個 Worker 完成任務。

並行 Worker 模型的另一個好處就是,它會將一個任務拆分紅多個小任務,併發執行,Delegator 在接受到 Worker 的處理結果後就會返回給 Client,整個 Worker -> Delegator -> Client 的過程是異步的。

並行 Worker 的缺點

一樣的,並行 Worker 模式一樣會有一些隱藏的缺點

「共享狀態會變得很複雜」

實際的並行 Worker 要比咱們圖中畫出的更復雜,主要是並行 Worker 一般會訪問內存或共享數據庫中的某些共享數據。

這些共享狀態可能會使用一些工做隊列來保存業務數據、數據緩存、數據庫的鏈接池等。在線程通訊中,線程須要確保共享狀態是否可以讓其餘線程共享,而不是僅僅停留在 CPU 緩存中讓本身可用,固然這些都是程序員在設計時就須要考慮的問題。線程須要避免 競態條件死鎖 和許多其餘共享狀態形成的併發問題。

多線程在訪問共享數據時,會丟失併發性,由於操做系統要保證只有一個線程可以訪問數據,這會致使共享數據的爭用和搶佔。未搶佔到資源的線程會 阻塞

現代的非阻塞併發算法能夠減小爭用提升性能,可是非阻塞算法比較難以實現。

可持久化的數據結構(Persistent data structures) 是另一個選擇。可持久化的數據結構在修改後始終會保留先前版本。所以,若是多個線程同時修改一個可持久化的數據結構,而且一個線程對其進行了修改,則修改的線程會得到對新數據結構的引用。

雖然可持久化的數據結構是一個新的解決方法,可是這種方法實行起來卻有一些問題,好比,一個持久列表會將新元素添加到列表的開頭,並返回所添加的新元素的引用,可是其餘線程仍然只持有列表中先前的第一個元素的引用,他們看不到新添加的元素。

持久化的數據結構好比 鏈表(LinkedList) 在硬件性能上表現不佳。列表中的每一個元素都是一個對象,這些對象散佈在計算機內存中。現代 CPU 的順序訪問每每要快的多,所以使用數組等順序訪問的數據結構則可以得到更高的性能。CPU 高速緩存能夠將一個大的矩陣塊加載到高速緩存中,並讓 CPU 在加載後直接訪問 CPU 高速緩存中的數據。對於鏈表,將元素分散在整個 RAM 上,這其實是不可能的。

「無狀態的 worker」

共享狀態能夠由其餘線程所修改,所以,worker 必須在每次操做共享狀態時從新讀取,以確保在副本上可以正確工做。不在線程內部保持狀態的 worker 成爲無狀態的 worker。

「做業順序是不肯定的」

並行工做模型的另外一個缺點是做業的順序不肯定,沒法保證首先執行或最後執行哪些做業。任務 A 在任務 B 以前分配給 worker,可是任務 B 可能在任務 A 以前執行。

流水線

第二種併發模型就是咱們常常在生產車間遇到的 流水線併發模型,下面是流水線設計模型的流程圖

這種組織架構就像是工廠中裝配線中的 worker,每一個 worker 只完成所有工做的一部分,完成一部分後,worker 會將工做轉發給下一個 worker。

每道程序都在本身的線程中運行,彼此之間不會共享狀態,這種模型也被稱爲無共享併發模型。

使用流水線併發模型一般被設計爲非阻塞I/O,也就是說,當沒有給 worker 分配任務時,worker 會作其餘工做。非阻塞I/O 意味着當 worker 開始 I/O 操做,例如從網絡中讀取文件,worker 不會等待 I/O 調用完成。由於 I/O 操做很慢,因此等待 I/O 很是耗費時間。在等待 I/O 的同時,CPU 能夠作其餘事情,I/O 操做完成後的結果將傳遞給下一個 worker。下面是非阻塞 I/O 的流程圖

在實際狀況中,任務一般不會按着一條裝配線流動,因爲大多數程序須要作不少事情,所以須要根據完成的不一樣工做在不一樣的 worker 之間流動,以下圖所示

任務還可能須要多個 worker 共同參與完成

響應式 - 事件驅動系統

使用流水線模型的系統有時也被稱爲 響應式 或者 事件驅動系統,這種模型會根據外部的事件做出響應,事件多是某個 HTTP 請求或者某個文件完成加載到內存中。

Actor 模型

在 Actor 模型中,每個 Actor 其實就是一個 Worker, 每個 Actor 都可以處理任務。

簡單來講,Actor 模型是一個併發模型,它定義了一系列系統組件應該如何動做和交互的通用規則,最著名的使用這套規則的編程語言是 Erlang。一個參與者Actor對接收到的消息作出響應,而後能夠建立出更多的 Actor 或發送更多的消息,同時準備接收下一條消息。

Channels 模型

在 Channel 模型中,worker 一般不會直接通訊,與此相對的,他們一般將事件發送到不一樣的 通道(Channel)上,而後其餘 worker 能夠在這些通道上獲取消息,下面是 Channel 的模型圖

有的時候 worker 不須要明確知道接下來的 worker 是誰,他們只須要將做者寫入通道中,監聽 Channel 的 worker 能夠訂閱或者取消訂閱,這種方式下降了 worker 和 worker 之間的耦合性。

流水線設計的優勢

與並行設計模型相比,流水線模型具備一些優點,具體優點以下

「不會存在共享狀態」

由於流水線設計可以保證 worker 在處理完成後再傳遞給下一個 worker,因此 worker 與 worker 之間不須要共享任何狀態,也就無需考慮併發問題。你甚至能夠在實現上把每一個 worker 當作是單線程的一種。

「有狀態 worker」

由於 worker 知道沒有其餘線程修改自身的數據,因此流水線設計中的 worker 是有狀態的,有狀態的意思是他們能夠將須要操做的數據保留在內存中,有狀態一般比無狀態更快。

「更好的硬件整合」

由於你能夠把流水線當作是單線程的,而單線程的工做優點在於它可以和硬件的工做方式相同。由於有狀態的 worker 一般在 CPU 中緩存數據,這樣能夠更快地訪問緩存的數據。

「使任務更加有效的進行」

能夠對流水線併發模型中的任務進行排序,通常用來日誌的寫入和恢復。

流水線設計的缺點

流水線併發模型的缺點是任務會涉及多個 worker,所以可能會分散在項目代碼的多個類中。所以很難肯定每一個 worker 都在執行哪一個任務。流水線的代碼編寫也比較困難,設計許多嵌套回調處理程序的代碼一般被稱爲 回調地獄。回調地獄很難追蹤 debug。


8

 函數性並行


函數性並行模型是最近才提出的一種併發模型,它的基本思路是使用函數調用來實現。消息的傳遞就至關因而函數的調用。傳遞給函數的參數都會被拷貝,所以在函數以外的任何實體都沒法操縱函數內的數據。這使得函數執行相似於原子操做。每一個函數調用均可以獨立於任何其餘函數調用執行。

當每一個函數調用獨立執行時,每一個函數均可以在單獨的 CPU 上執行。這也就是說,函數式並行並行至關因而各個 CPU 單獨執行各自的任務。

JDK 1.7 中的 ForkAndJoinPool 類就實現了函數性並行的功能。Java 8 提出了 stream 的概念,使用並行流也可以實現大量集合的迭代。

函數性並行的難點是要知道函數的調用流程以及哪些 CPU 執行了哪些函數,跨 CPU 函數調用會帶來額外的開銷。

咱們以前說過,線程就是進程中的一條順序流,在 Java 中,每一條 Java 線程就像是 JVM 的一條順序流,就像是虛擬 CPU 同樣來執行代碼。Java 中的 main() 方法是一條特殊的線程,JVM 建立的 main 線程是一條主執行線程,在 Java 中,方法都是由 main 方法發起的。在 main 方法中,你照樣能夠建立其餘的線程(執行順序流),這些線程能夠和 main 方法共同執行應用代碼。

Java 線程也是一種對象,它和其餘對象同樣。Java 中的 Thread 表示線程,Thread 是 java.lang.Thread 類或其子類的實例。那麼下面咱們就來一塊兒探討一下在 Java 中如何建立和啓動線程。


9

 建立並啓動線程 


在 Java 中,建立線程的方式主要有三種

  • 經過繼承 Thread 類來建立線程
  • 經過實現 Runnable 接口來建立線程
  • 經過 CallableFuture 來建立線程

下面咱們分別探討一下這幾種建立方式

繼承 Thread 類來建立線程

第一種方式是繼承 Thread 類來建立線程,以下示例

public class TJavaThread extends Thread{

    static int count;

    @Override
    public synchronized void run() {
        for(int i = 0;i < 10000;i++){
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        TJavaThread tJavaThread = new TJavaThread();
        tJavaThread.start();
        tJavaThread.join();
        System.out.println("count = " + count);
    }
}

線程的主要建立步驟以下

  • 定義一個線程類使其繼承 Thread 類,並重寫其中的 run 方法,run 方法內部就是線程要完成的任務,所以 run 方法也被稱爲 執行體
  • 建立了 Thread 的子類,上面代碼中的子類是 TJavaThread
  • 啓動方法須要注意,並非直接調用 run 方法來啓動線程,而是使用 start 方法來啓動線程。固然 run 方法能夠調用,這樣的話就會變成普通方法調用,而不是新建立一個線程來調用了。
public static void main(String[] args) throws InterruptedException {

  TJavaThread tJavaThread = new TJavaThread();
  tJavaThread.run();
  System.out.println("count = " + count);
}

這樣的話,整個 main 方法只有一條執行線程也就是 main 線程,由兩條執行線程變爲一條執行線程

Thread 構造器只須要一個 Runnable 對象,調用 Thread 對象的 start() 方法爲該線程執行必須的初始化操做,而後調用 Runnable 的 run 方法,以便在這個線程中啓動任務。咱們上面使用了線程的 join 方法,它用來等待線程的執行結束,若是咱們不加 join 方法,它就不會等待 tJavaThread 的執行完畢,輸出的結果可能就不是 10000

能夠看到,在 run  方法尚未結束前,run 就被返回了。也就是說,程序不會等到 run 方法執行完畢就會執行下面的指令。

使用繼承方式建立線程的優點:編寫比較簡單;可使用 this 關鍵字直接指向當前線程,而無需使用 Thread.currentThread() 來獲取當前線程。

使用繼承方式建立線程的劣勢:在 Java 中,只容許單繼承(拒絕肛精說使用內部類能夠實現多繼承)的原則,因此使用繼承的方式,子類就不能再繼承其餘類。

使用 Runnable 接口來建立線程

相對的,還可使用 Runnable 接口來建立線程,以下示例

public class TJavaThreadUseImplements implements Runnable{

    static int count;

    @Override
    public synchronized void run() {
        for(int i = 0;i < 10000;i++){
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        new Thread(new TJavaThreadUseImplements()).start();
        System.out.println("count = " + count);
    }

}

線程的主要建立步驟以下

  • 首先定義 Runnable 接口,並重寫 Runnable 接口的 run 方法,run 方法的方法體一樣是該線程的線程執行體。
  • 建立線程實例,可使用上面代碼這種簡單的方式建立,也能夠經過 new 出線程的實例來建立,以下所示
TJavaThreadUseImplements tJavaThreadUseImplements = new TJavaThreadUseImplements();
new Thread(tJavaThreadUseImplements).start();
  • 再調用線程對象的 start 方法來啓動該線程。

線程在使用實現 Runnable 的同時也能實現其餘接口,很是適合多個相同線程來處理同一份資源的狀況,體現了面向對象的思想。

使用 Runnable 實現的劣勢是編程稍微繁瑣,若是要訪問當前線程,則必須使用 Thread.currentThread() 方法。

使用 Callable 接口來建立線程

Runnable 接口執行的是獨立的任務,Runnable 接口不會產生任何返回值,若是你但願在任務完成後可以返回一個值的話,那麼你能夠實現 Callable 接口而不是 Runnable 接口。Java SE5 引入了 Callable 接口,它的示例以下

public class CallableTask implements Callable {

    static int count;
    public CallableTask(int count){
        this.count = count;
    }

    @Override
    public Object call() {
        return count;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        FutureTask<Integer> task = new FutureTask((Callable<Integer>) () -> {
            for(int i = 0;i < 1000;i++){
                count++;
            }
            return count;
        });
        Thread thread = new Thread(task);
        thread.start();

        Integer total = task.get();
        System.out.println("total = " + total);
    }
}

我想,使用 Callable 接口的好處你已經知道了吧,既可以實現多個接口,也可以獲得執行結果的返回值。Callable 和 Runnable 接口仍是有一些區別的,主要區別以下

  • Callable 執行的任務有返回值,而 Runnable 執行的任務沒有返回值
  • Callable(重寫)的方法是 call 方法,而 Runnable(重寫)的方法是 run 方法。
  • call 方法能夠拋出異常,而 Runnable 方法不能拋出異常

使用線程池來建立線程

首先先來認識一下頂級接口 Executor,Executor 雖然不是傳統線程建立的方式之一,可是它卻成爲了建立線程的替代者,使用線程池的好處以下

  • 利用線程池可以複用線程、控制最大併發數。
  • 實現任務線程隊列 緩存策略拒絕機制
  • 實現某些與時間相關的功能,如定時執行、週期執行等。
  • 隔離線程環境。好比,交易服務和搜索服務在同一臺服務器上,分別開啓兩個線程池,交易線程的資源消耗明顯要大;所以,經過配置獨立的線程池,將較慢的交易服務與搜索服務隔開,避免服務線程互相影響。

你可使用以下操做來替換線程建立

new Thread(new(RunnableTask())).start()

// 替換爲
  
Executor executor = new ExecutorSubClass() // 線程池實現類;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());

ExecutorService 是 Executor 的默認實現,也是 Executor 的擴展接口,ThreadPoolExecutor 類提供了線程池的擴展實現。Executors 類爲這些 Executor 提供了方便的工廠方法。下面是使用 ExecutorService 建立線程的幾種方式

CachedThreadPool

從而簡化了併發編程。Executor 在客戶端和任務之間提供了一個間接層;與客戶端直接執行任務不一樣,這個中介對象將執行任務。Executor 容許你管理異步任務的執行,而無須顯示地管理線程的生命週期。

public static void main(String[] args) {
  ExecutorService service = Executors.newCachedThreadPool();
  for(int i = 0;i < 5;i++){
    service.execute(new TestThread());
  }
  service.shutdown();
}

CachedThreadPool 會爲每一個任務都建立一個線程。

注意:ExecutorService 對象是使用靜態的 Executors 建立的,這個方法能夠肯定 Executor 類型。對 shutDown 的調用能夠防止新任務提交給 ExecutorService ,這個線程在 Executor 中全部任務完成後退出。

FixedThreadPool

FixedThreadPool 使你可使用有限的線程集來啓動多線程

public static void main(String[] args) {
  ExecutorService service = Executors.newFixedThreadPool(5);
  for(int i = 0;i < 5;i++){
    service.execute(new TestThread());
  }
  service.shutdown();
}

有了 FixedThreadPool 使你能夠一次性的預先執行高昂的線程分配,所以也就能夠限制線程的數量。這能夠節省時間,由於你沒必要爲每一個任務都固定的付出建立線程的開銷。

SingleThreadExecutor

SingleThreadExecutor 就是線程數量爲 1的 FixedThreadPool,若是向 SingleThreadPool 一次性提交了多個任務,那麼這些任務將會排隊,每一個任務都會在下一個任務開始前結束,全部的任務都將使用相同的線程。SingleThreadPool 會序列化全部提交給他的任務,並會維護它本身(隱藏)的懸掛隊列。

public static void main(String[] args) {
  ExecutorService service = Executors.newSingleThreadExecutor();
  for(int i = 0;i < 5;i++){
    service.execute(new TestThread());
  }
  service.shutdown();
}

從輸出的結果就能夠看到,任務都是挨着執行的。我爲任務分配了五個線程,可是這五個線程不像是咱們以前看到的有換進換出的效果,它每次都會先執行完本身的那個線程,而後餘下的線程繼續走完這條線程的執行路徑。你能夠用 SingleThreadExecutor 來確保任意時刻都只有惟一一個任務在運行。

休眠

影響任務行爲的一種簡單方式就是使線程 休眠,選定給定的休眠時間,調用它的 sleep() 方法, 通常使用的TimeUnit 這個時間類替換 Thread.sleep() 方法,示例以下:

public class SuperclassThread extends TestThread{

    @Override
    public void run() {
        System.out.println(Thread.currentThread() + "starting ..." );

        try {
            for(int i = 0;i < 5;i++){
                if(i == 3){
                    System.out.println(Thread.currentThread() + "sleeping ...");
                    TimeUnit.MILLISECONDS.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread() + "wakeup and end ...");
    }

    public static void main(String[] args) {
        ExecutorService executors = Executors.newCachedThreadPool();
        for(int i = 0;i < 5;i++){
            executors.execute(new SuperclassThread());
        }
        executors.shutdown();
    }
}

關於 TimeUnit 中的 sleep() 方法和 Thread.sleep() 方法的比較,請參考下面這篇博客

(https://www.cnblogs.com/xiadongqing/p/9925567.html)

優先級

上面提到線程調度器對每一個線程的執行都是不可預知的,隨機執行的,那麼有沒有辦法告訴線程調度器哪一個任務想要優先被執行呢?你能夠經過設置線程的優先級狀態,告訴線程調度器哪一個線程的執行優先級比較高,「請給這個騎手立刻派單」,線程調度器傾向於讓優先級較高的線程優先執行,然而,這並不意味着優先級低的線程得不到執行,也就是說,優先級不會致使死鎖的問題。優先級較低的線程只是執行頻率較低。

public class SimplePriorities implements Runnable{

    private int priority;

    public SimplePriorities(int priority) {
        this.priority = priority;
    }

    @Override
    public void run() {
        Thread.currentThread().setPriority(priority);
        for(int i = 0;i < 100;i++){
            System.out.println(this);
            if(i % 10 == 0){
                Thread.yield();
            }
        }
    }

    @Override
    public String toString() {
        return Thread.currentThread() + " " + priority;
    }

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        for(int i = 0;i < 5;i++){
            service.execute(new SimplePriorities(Thread.MAX_PRIORITY));
        }
        service.execute(new SimplePriorities(Thread.MIN_PRIORITY));
    }
}

toString() 方法被覆蓋,以便經過使用 Thread.toString() 方法來打印線程的名稱。你能夠改寫線程的默認輸出,這裏採用了 「Thread[pool-1-thread-1,10,main]」 這種形式的輸出。

經過輸出,你能夠看到,最後一個線程的優先級最低,其他的線程優先級最高。注意,優先級是在 run 開頭設置的,在構造器中設置它們不會有任何好處,由於這個時候線程尚未執行任務。

儘管 JDK 有 10 個優先級,可是通常只有「MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY」 三種級別。

做出讓步

咱們上面提過,若是知道一個線程已經在 run() 方法中運行的差很少了,那麼它就能夠給線程調度器一個提示:我已經完成了任務中最重要的部分,可讓給別的線程使用 CPU 了。這個暗示將經過 yield() 方法做出。

有一個很重要的點就是,Thread.yield() 是建議執行切換CPU,而不是強制執行CPU切換。

對於任何重要的控制或者在調用應用時,都不能依賴於 yield()方法,實際上, yield() 方法常常被濫用。

後臺線程

後臺(daemon)線程,是指運行時在後臺提供的一種服務線程,這種線程不是屬於必須的。當全部非後臺線程結束時,程序也就中止了,**同時會終止全部的後臺線程。**反過來講,只要有任何非後臺線程還在運行,程序就不會終止。

public class SimpleDaemons implements Runnable{

    @Override
    public void run() {
        while (true){
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread() + " " + this);
            } catch (InterruptedException e) {
                System.out.println("sleep() interrupted");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i = 0;i < 10;i++){
            Thread daemon = new Thread(new SimpleDaemons());
            daemon.setDaemon(true);
            daemon.start();
        }
        System.out.println("All Daemons started");
        TimeUnit.MILLISECONDS.sleep(175);
    }
}

在每次的循環中會建立 10 個線程,並把每一個線程設置爲後臺線程,而後開始運行,for 循環會進行十次,而後輸出信息,隨後主線程睡眠一段時間後中止運行。在每次 run 循環中,都會打印當前線程的信息,主線程運行完畢,程序就執行完畢了。由於 daemon 是後臺線程,沒法影響主線程的執行。

可是當你把 daemon.setDaemon(true) 去掉時,while(true) 會進行無限循環,那麼主線程一直在執行最重要的任務,因此會一直循環下去沒法中止。

ThreadFactory

按須要建立線程的對象。使用線程工廠替換了 Thread 或者 Runnable 接口的硬鏈接,使程序可以使用特殊的線程子類,優先級等。通常的建立方式爲

class SimpleThreadFactory implements ThreadFactory {
  public Thread newThread(Runnable r) {
    return new Thread(r);
  }
}

Executors.defaultThreadFactory 方法提供了一個更有用的簡單實現,它在返回以前將建立的線程上下文設置爲已知值

ThreadFactory是一個接口,它只有一個方法就是建立線程的方法

public interface ThreadFactory {

    // 構建一個新的線程。實現類可能初始化優先級,名稱,後臺線程狀態和 線程組等
    Thread newThread(Runnable r);
}

下面來看一個 ThreadFactory 的例子

public class DaemonThreadFactory implements ThreadFactory {

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setDaemon(true);
        return t;
    }
}

public class DaemonFromFactory implements Runnable{

    @Override
    public void run() {
        while (true){
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread() + " " + this);
            } catch (InterruptedException e) {
                System.out.println("Interrupted");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool(new DaemonThreadFactory());
        for(int i = 0;i < 10;i++){
            service.execute(new DaemonFromFactory());
        }
        System.out.println("All daemons started");
        TimeUnit.MILLISECONDS.sleep(500);
    }
}

Executors.newCachedThreadPool 能夠接受一個線程池對象,建立一個根據須要建立新線程的線程池,但會在它們可用時重用先前構造的線程,並在須要時使用提供的 ThreadFactory 建立新線程。

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

加入一個線程

一個線程能夠在其餘線程上調用 join() 方法,其效果是等待一段時間直到第二個線程結束才正常執行。若是某個線程在另外一個線程 t 上調用 t.join() 方法,此線程將被掛起,直到目標線程 t 結束纔回復(能夠用 t.isAlive() 返回爲真假判斷)。

也能夠在調用 join 時帶上一個超時參數,來設置到期時間,時間到期,join方法自動返回。

對 join 的調用也能夠被中斷,作法是在線程上調用 interrupted 方法,這時須要用到 try...catch 子句

public class TestJoinMethod extends Thread{

    @Override
    public void run() {
        for(int i = 0;i < 5;i++){
            try {
                TimeUnit.MILLISECONDS.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("Interrupted sleep");
            }
            System.out.println(Thread.currentThread() + " " + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestJoinMethod join1 = new TestJoinMethod();
        TestJoinMethod join2 = new TestJoinMethod();
        TestJoinMethod join3 = new TestJoinMethod();

        join1.start();
//        join1.join();

        join2.start();
        join3.start();
    }
}

join() 方法等待線程死亡。換句話說,它會致使當前運行的線程中止執行,直到它加入的線程完成其任務。

線程異常捕獲

因爲線程的本質,使你不能捕獲從線程中逃逸的異常,一旦異常逃出任務的 run 方法,它就會向外傳播到控制檯,除非你採起特殊的步驟捕獲這種錯誤的異常,在 Java5 以前,你能夠經過線程組來捕獲,可是在 Java 5 以後,就須要用 Executor 來解決問題,由於線程組不是一次好的嘗試。

下面的任務會在 run 方法的執行期間拋出一個異常,而且這個異常會拋到 run 方法的外面,並且 main 方法沒法對它進行捕獲

public class ExceptionThread implements Runnable{

    @Override
    public void run() {
        throw new RuntimeException();
    }

    public static void main(String[] args) {
        try {
            ExecutorService service = Executors.newCachedThreadPool();
            service.execute(new ExceptionThread());
        }catch (Exception e){
            System.out.println("eeeee");
        }
    }
}

爲了解決這個問題,咱們須要修改 Executor 產生線程的方式,Java5 提供了一個新的接口 Thread.UncaughtExceptionHandler ,它容許你在每一個 Thread 上都附着一個異常處理器。Thread.UncaughtExceptionHandler.uncaughtException() 會在線程因未捕獲臨近死亡時被調用。

public class ExceptionThread2 implements Runnable{

    @Override
    public void run() {
        Thread t = Thread.currentThread();
        System.out.println("run() by " + t);
        System.out.println("eh = " + t.getUncaughtExceptionHandler());
      
       // 手動拋出異常
        throw new RuntimeException();
    }
}

// 實現Thread.UncaughtExceptionHandler 接口,建立異常處理器
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught " + e);
    }
}

public class HandlerThreadFactory implements ThreadFactory {

    @Override
    public Thread newThread(Runnable r) {
        System.out.println(this + " creating new Thread");
        Thread t = new Thread(r);
        System.out.println("created " + t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        System.out.println("ex = " + t.getUncaughtExceptionHandler());
        return t;
    }
}

public class CaptureUncaughtException {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool(new HandlerThreadFactory());
        service.execute(new ExceptionThread2());
    }
}

在程序中添加了額外的追蹤機制,用來驗證工廠建立的線程會傳遞給UncaughtExceptionHandler,你能夠看到,未捕獲的異常是經過 uncaughtException 來捕獲的。


       
   
爲何阿里巴巴 Java 開發手冊禁止使用 Apache Beanutils 進行屬性的 copy?
不可不知的 9 道 Java 集合面試題
不會用 Java Future,我懷疑你泡茶都沒我快

以爲不錯,點個在看~

本文分享自微信公衆號 - 武培軒(wupeixuan404)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索