併發編程 - 探索一

0x01 什麼是併發

要理解併發首選咱們來區分下併發和並行的概念。java

併發:表示在一段時間內有多個動做存在。
並行:表示在同一時間點有多個動做同時存在。編程

例如:
此刻我正在寫博客,可是我寫着寫着停下來吃一下東西(菠蘿片)再寫、再吃。這兩個動做在一段時間內都在發生着,這能夠理解爲併發。
另外一方面我在寫這個博客的同時我在聽音樂。那麼同時存在的兩個動做(寫博客、聽音樂)是同時在發生的這就是所謂的並行。安全

從上面兩個概念明顯能夠感覺到併發是包含並行操做。因此咱們一般說的併發編程對於cpu來講有多是併發的在執行也有多是交替的在執行。多線程


說到這裏你可能會問爲何咱們須要併發編程?
在求解單個問題的時候凡是涉及多個執行流程的編程模式都叫併發編程。併發

0x02 爲何須要併發

  1. 硬件的發展推進軟件的進度,多核時代的到來ide

  2. 應用系統對性能和吞吐量的苛刻要求性能

  3. 大數據時代的到來測試

  4. 移動互聯網、雲計算對計算體系的衝擊大數據

0x03 併發編程方式

Java:多進程/多線程的併發實現方式編碼

Go:協程--用戶態實現的多線程方式(goroutine)

Java併發模型

在介紹java併發模型前咱們來介紹下系統對多線程的實現方式。系統支持用戶態線程和內核態兩種線程的實現方式,內核態線程是cpu去調度的最小單位,因此這牽涉到用戶態線程和內核態線程之間的映射關係,用戶態線程:內核態線程 = 1:1 、 N:1 、 M:N。

1:1 這種映射關係充分利用多核的優點,可是這種方式在用戶態進行線程切換的過程當中都會涉及到內核態線程之間的切換,切換開銷大。(主要涉及內核線程運行時上下文的保存與恢復)
N:1 無法充分利用多核的優點,可是這種因爲是用戶態的內存切換不涉及內核態線程之間的切換因此這種映射關係在線程之間切換代價小。
M:N 這種是上面兩種映射關係的結合體,集合了上面兩種映射關係的優點,可是這也增長了線程之間這種映射關係的調度複雜度。

Java的併發編程模式是經過1:1這種映射關係來實現線程之間的併發調度。

Go併發模型

Go的併發模式是經過M:N這種方式來實現併發調度的。
Go調度器中有三種重要結構:M(posix thread)、P(調度上下文,通常數量設置爲和機器內核數相同,這樣能充分發揮機器的併發性能)、G(goroutine)。

clipboard.png

一個調度上下文能夠包含多個Goroutine,多個上下文因此能夠全部的Goroutine都能併發的運行在CPU的多核上面。
若是有Goroutine發現找不到調度上下文,就會被放到global runqueue中,等悠閒的調度上下文來撈取它進行調度。
若是調度上下文上面掛載的全部Goroutine都已經執行完畢,此時他會去global runqueue中獲取Goroutine,若是發現此時沒有獲取到,則會去別的調度上文中搶Goroutine,通常一次搶都是搶此時被搶調度上下文的一半Goroutine,確保充分利用M去被多核調度。

0x04 併發帶來的問題

在享受併發編程帶來的高性能、高吞吐量的同時,也會由於併發編程帶來一些意想不到弊端。

  1. 資源的消耗,要管理這麼多用戶線程、內核線程、用戶線程內核線程之間的切換調度,上下文等等這些都是因爲引用了併發編程所帶來的額外消耗。

  2. 併發過程當中多線程之間的切換調度,上下文的保存恢復等都會帶來額外的線程切換開銷。

  3. 編碼、測試的複雜性。和咱們生活中的例子很相像,三五我的一塊兒出去活動很容易把控,若是帶着幾10、上百人的團隊出去活動這些都會帶來額外的管理上的開銷。

真的是有陽關的地方就有黑暗啊!

上面這些都是咱們無法避免的一些問題,要引用併發編程必然會要付出點額外的代價才行。可是併發編程還帶來了一個不能忽視的問題,線程之間對同一資源的競爭訪問,形成內存對象狀態和本身的想象千差萬別。

Java線程和內存對象之間的交互

java線程對內存的理解分爲兩部分:線程工做內存(每一個線程獨有的)、共享內存也叫主內存(全部的線程所共有的),下面是java線程對內存中Count對象的一次修改操做。

clipboard.png

從主線程中讀取Count對象放入線程工做內存,後面的讀取修改都在線程工做內存中,最後(更新到主內存的時間不是肯定的,可能會插入別的操做在store、write之間)更新到主內存中。全部的上述操做都是順序執行的,可是不保證連續執行。

volatile變量、synchronized同步塊

volatile變量、synchronized塊執行結束後能保證每次去更新的值都會當即寫入到主內存中。
volatile變量不少人會認爲這樣就是線程安全的,可是經過上面咱們能夠看到若是兩個線程同時去讀了一個volatile變量,最後一前一後更新到主內存中,這樣也會出現寫丟失的狀況,因此volatile不能保證線程安全。

0x05 併發實戰

1) 定義線程池

private static final ExecutorService executor = Executors.newFixedThreadPool(20);

2)定義併發服務

CompletionService<Result> completionService = new 
      ExecutorCompletionService<Result>(executor);

3)提交併發任務

completionService.submit(new Callable<void>() {
    @Override
    public void call() throws Exception {
        return ;
    }
});

4)等待併發結果

for (int i = 0; i < taskSize; ++i) {
    Future<ModelScoreResult> future = completionService.poll(TIME_OUT,
                    TimeUnit.SECONDS);
    Result result = future.get();
}
相關文章
相關標籤/搜索