Java併發原理無廢話指南
Java併發原理
看了一篇java併發原理的文章大受啟發,特此分享一下!
作者的原意:寫文章的目的是希望用“基礎”知識來解釋併發程式設計的問題,而不是像“某些”文章一樣一上來就擺各種名詞,各種JVM記憶體模型,各種Java規範。我覺得後者只能讓人更困惑,有時候“基礎”的力量非常強大。
網上有不計其數的併發程式設計文章,甚至有不計其數的書來介紹這個主題。你為什麼要花10分鐘時間來讀完這篇文章呢?我給的答案:“他們全是廢話。”,我覺得這個主題用10分鐘就可以說完,根本不要用花這麼長時間,也不用去折騰Java記憶體模型之類的東西。
理解Java併發原理或者其他語言的併發,只需要記住理解兩個東西:
1. CPU訪問儲存的方式——多級儲存;
2. CPU執行指令的方式——亂序
首先回憶我們大學的一門課程——《計算機組成原理》也許你的記憶裡只有:“呃,你要說xx進位制轉換成xx進位制嗎?”。沒關係我幫你回憶一下:
1. 有一節課講多級儲存,說計算機最快的儲存是CPU裡面的Cache,其次是記憶體,最後是硬碟,最次的是外部儲存(比如光碟之類的)。
2. 還有一節課講的是CPU流水線,亂序執行、分支預測,說CPU考慮效能問題會把幾個沒有資料關聯的指令打亂順序執行。
多級儲存
我們來看一個“無聊的”Java例子:
程式定義了一個執行緒,執行緒會不停的判斷stop標誌位,如果為真則迴圈累加i。然後我們在主執行緒裡面修改stop為true。期望執行緒在進行2秒之後停止。
如果執行這個程式我們得到的結果是——程式永遠不會停止。主執行緒裡面修改的變數在testThread裡面並沒有發生改變。
解釋這個程式就用到了“多級儲存”,在x86架構的CPU中對資料的的訪問都是經過暫存器,如果資料在記憶體中CPU會先載入到暫存器然後在讀取;寫入的時候CPU只寫入到暫存器,在“適當的時候”資料會被回寫到記憶體中。畫個圖:
作業系統把我們程式中的主程序和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效率低,無法保證原子性所以不安全).
流水線,亂序執行、分支預測
我定義了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嗎?)。