09.11 Java併發原理

網上有不計其數的併發編程文章,甚至有不計其數的書來介紹這個主題。你為什麼要花10分鐘時間來讀完這篇文章呢?我給的答案:“他們全是廢話。”,我覺得這個主題用10分鐘就可以說完,

根本不要用花這麼長時間,也不用去折騰Java內存模型之類的東西。

我只講原理,不會告訴你怎麼用Java的併發庫,這是java doc乾的事情

理解Java併發原理或者其他語言的併發(沒錯,這篇文章是“跨語言”的!!!還這麼短,你說牛逼不牛逼)只需要記住理解兩個東西:

  1. CPU訪問存儲的方式——多級存儲;
  2. CPU執行指令的方式——亂序

首先回憶我們大學的一門課程——《計算機組成原理》也許你的記憶裡只有:“呃,你要說xx進制轉換成xx進制嗎?”。沒關係我幫你回憶一下:

  • 有一節課講多級存儲,說計算機最快的存儲是CPU裡面的Cache,其次是內存,最後是硬盤,最次的是外部存儲(比如光盤之類的)。
  • 還有一節課講的是CPU流水線,亂序執行、分支預測,說CPU考慮性能問題會把幾個沒有數據關聯的指令打亂順序執行。

怎麼樣?有印象了嗎?(什麼?沒讀過大學?那我覺得你有必要讀一下大學的課程——即便你不想混文憑)。

多級存儲

我們來看一個“無聊的”Java例子(例子沒有任何意義,會枯燥一些,耐著性質你讀懂了可以超脫了)

Java併發原理


程序定義了一個線程,線程會不停的判斷stop標誌位,如果為真則循環累加i。然後我們在主線程裡面修改stop為true。期望線程在進行2秒之後停止。

如果運行這個程序我們得到的結果是——程序永遠不會停止。主線程裡面修改的變量在testThread裡面並沒有發生改變。

解釋這個程序就用到了“多級存儲”,在x86架構的CPU中對數據的的訪問都是經過寄存器,如果數據在內存中CPU會先加載到寄存器然後在讀取;寫入的時候CPU只寫入到寄存器,在“適當的時候”數據會被回寫到內存中。畫個圖:

Java併發原理


操作系統把我們程序中的主進程和testThread調度到不同的CPU,testThread(CPU1)訪問stop的時候數據被複制到Cache中然後讀取;主進程(CPU2)訪問stop的時候數據被複制到Cache中然後讀取,賦值的時候會寫入到Cache中。所以CPU2修改的值並不會立馬被CPU1看到,這取決於:

  • CPU2是不是寫回到內存中;
  • CPU1的Cache是不是被“淘汰”重新從內存中加載數據;

第一條比較容易滿足,因為Cache必定會回寫到內存中(只不過不是實時寫入);第二條看起來比較困難,唯一的解決辦法是我們訪問stop變量的時候每次都從內存加載而不是通過Cache。在Java中實現這個功能的關鍵字是volatile。

public static volatile boolean stop = false;

這樣程序就可以“正常”執行了。需要注意,volatile只保證“好吧,我不用Cache”,無法保證原子性(比如賦值操作被拆分為多個CPU指令,那麼其他進程可能看到的是一個“中間結果”)。所以volatile其實是一種低效、不安全的併發處理方式。(不使用Cache效率低,無法保證原子性所以不安全)。

流水線,亂序執行、分支預測

代碼比上一個更加枯燥,忍耐一下:

Java併發原理


我定義了4個變量,兩個線程,然後分別啟動兩個線程,等待線程執行完之後輸出x,y的值。同志們可以猜猜結果是多少。(註釋後面的標號代表語句編號)

沒錯,根本沒有“正確”答案。我這裡有四種答案:

  • 結果:x=0, y=1;執行順序:1, 2, 3, 4
  • 結果:x=1, y=0;執行順序:3, 4, 1, 2
  • 結果:x=1, y=1;執行順序:1, 3, 2, 4
  • 結果:x=0, y=0;執行順序:2, 4, 1, 3

(前面三種執行結果你多執行幾次都會出現,後面的理論是存在。但是我沒有執行出來,單顆CPU更容易出現這樣的結果)

這就是併發的本質,你的代碼不會按照你寫順序執行。前三個很容解釋,兩個線程可能會被“交替”執行,讓人困惑的是第四個結果,解釋這個就必須用到“流水線,亂序執行、分支預測”。

CPU內部有多個執行單元(如果是多個CPU那就更多執行單元了),為了提高吞吐量,它會採用流水線同時執行多條指令;為了優化程序執行的效率適應流水線,CPU會分析指令的依賴關係把可以並行執行的指令並行執行。

在one線程中,a=1和y=b是沒有任何依賴關係的,所以可能y=b會被先執行,a=1則後執行。同樣的道理other線程中也是如此。

總結

沒錯,存儲訪問引起的不一致性+CPU為了提高效率引入的並行機制就是併發程序設計的困難,這兩個問題結合在一起就是“Memory barrier”(內存屏障、內存柵欄),這不是Java獨有的,在任何編程語言中都會存在這個問題,除非你的CPU不是多級存儲、沒有流水線(這還是CPU嗎?)。

寫這篇文章的目的是希望用“基礎”知識來解釋併發編程的問題,而不是像“某些”文章一樣一上來就擺各種名詞,各種JVM內存模型,各種Java規範。我覺得後者只能讓人更困惑,有時候“基礎”的力量非常強大。希望這篇文章對大家有幫助。


分享到:


相關文章: