打工人!肝了這套多執行緒吧!壹

羅拉快跑跑跑跑發表於2020-11-27

開篇閒扯

    一年又一年,年年多執行緒。不論你是什麼程式設計師,都逃脫不了多執行緒併發的魔爪。因為它從盤古開天闢地的時候就有了,就是在計算機中對現實世界的一種抽象。因此,放輕鬆別害怕,肝了這系列的多執行緒文章,差不多能吊打面試官了(可別真動手...)。

併發症

    併發問題,曾經在單核單執行緒的機器上是不存在的(不是不想,是做不到)。假如把計算機看成一個木桶,那麼跟我們Java開發人員關係最大的就是CPU、記憶體、IO裝置。這三塊木板發展至今,彼此之間也形成了較大的效能差異。CPU的核心數執行緒數在不斷增多,記憶體的速度卻跟不上CPU的步伐,同理IO裝置也沒能跟上記憶體的步伐。於是就加快取,經過科學論證三級快取最靠譜,於是就有了常見的CPU三級快取。然後前輩們再對作業系統做各類排程層面的深度優化,通過軟硬兼施的手法,使得軟體與硬體的完美結合,才有如今繁榮的網際網路。而我們不過是在這座城市裡的打工人罷了。
言歸正傳,本文將分別說明在併發世界裡的“三宗罪”:可見性原子性有序性


罪狀一:可見性

前文中有說到CPU的發展經歷了從單核單執行緒到現在的多核心多執行緒,而記憶體的讀寫效能卻供應不上CPU的處理能力,於是就增加了快取,至於前文中提到的三級快取為什麼是三級,不在本文討論範圍,有興趣自己看去。。。

為什麼會有可見性問題?

    在單核心時代,所有的執行緒都是交給一個CPU序列執行,因此不論有多少執行緒都是排隊執行,也就不會形成執行緒A與B同時競爭target變數的競爭狀態,如圖一。
image
    來到多核心多執行緒時代,每顆CPU都有各自的快取,如果多個執行緒分別在不同的CPU上執行,且需要同時操作同一個資料。而每顆CPU在處理記憶體中的資料時,會先將目標資料快取到CPU快取中。這時候CPU們各幹各的,也不管目標值有沒有被其他CPU修改過,自己在快取中修改後不管三七二十一就寫回去,這肯定是不行的啊兄弟...,而這就是我們Java中常說的資料可見性問題,再追根溯源就是:CPU級別的快取一致性協議。後邊文章會詳細解釋(別問具體時間,問了就是明天)。

可見性問題怎麼解決?

    這個簡單,如果僅僅是解決可見性,那就Volatile關鍵字用起來(也不是萬能的,慎重考慮),它會將共享變數資料從執行緒工作記憶體重新整理到主存中,而這個關鍵字的實現基礎是Java規範的記憶體模型,注意,這裡要和JVM記憶體模型區分開,兩者是不一樣的東西。那麼Java記憶體模型又是什麼樣的,為啥設計這個記憶體模型,有哪些好處?下篇詳細解釋!本文就先放一張簡單的圖:
image


罪狀二:原子性

    大家都知道CPU的執行時間是分片進行的,可能CPU這段時間在執行我寫的if-else,下一時刻由於作業系統的排程當前執行緒丟失時間片,又執行其他執行緒任務去了(呸!渣男)。打斷了我當前執行緒的一個或者多個操作流程,這就是原子性被破壞了,也就是多執行緒無鎖情況下的ABA問題。跟我們期望的完全不一樣啊,還是看圖說話:
image
    解釋一下就是:想要得到temp為2的結果,但是隻能得到1,原因就是執行A執行緒的CPU幹別的去了,而這時候B執行緒所在的CPU後發制A,搶先完成了++的操作並寫回記憶體,但是A不知道,還傻傻的以為它的到的是temp的初戀,又傻傻的寫會去,然後就心態崩了呀!偷襲~(出錯)


罪狀三:有序性

    如果說原子性問題是硬體工程師挖的坑(CPU別切換多好),那有序性就妥妥的是軟體工程師下了老鼠夾子(誇張了啊,其實都是為了效率)。之所以存在有序性問題,完全是編譯大神們對我們的關愛,知道我們普通Coder對效能的要求是能跑就行。

    因此,在Java程式碼在編譯時期動了手腳,比如說:鎖消除、鎖粗化(需要進行逃逸性分析等技術手段)或者是將A、B兩段操作互換順序。但是,所有的這一切都不能影響原始碼在單執行緒執行情況下的最終結果,即as-if-serial語義。這是個很頂層的協議,不論是編譯器、執行時狀態還是處理器都必須遵守該語義。這是保證程式正確性的大前提。當然,編譯器不僅僅要準守as-if-serial語義,還要準守以下八大規則--Happens-Before規則(八仙過海各顯神通):

什麼是Happens-Before規則?

    一段程式中,前面執行後的結果,對後面的操作來說均可見。即:不論怎麼編譯優化(編譯優化的文章以後會寫,關注我,全免費)都不能違背這一指導思想。不能忘本

規則名稱 解釋
程式順序規則 在一個執行緒中,按照程式的順序,前面的操作先發生於後續的操作
volatile變數規則 對volatile變數進行寫操作時,要優先發生於對這個變數的讀操作,可以理解為禁止指令重排但實際不完全是一個意思
執行緒start()規則 很好理解,執行緒的start()操作要優先發生於該執行緒中的所有操作(要先有雞才能有蛋)
執行緒join()規則 執行緒A呼叫執行緒B的join()併成功返回結果時,執行緒B的任意操作都是先於join()操作的。
管理鎖定規則 在java中以Synchronized為例來說就是加解鎖操作要成對且解鎖操作在加鎖之後
物件終結規則 一個物件的初始化完成happen—before它的finalize()方法的開始
傳遞性 即A操作先於B發生,B先於C發==>A先於C發生

:文章裡所有類似“先於”、“早於”等詞並不嚴謹不能和Happens-Before劃等號,只是這樣說更好理解,較為準確的含義是:操作結果對後者可見。

其實,總結來說就是JMM、編譯器和程式設計師之間的關係。

JMM對程式設計師說:你按順序寫,執行結果就是按照你寫的順序執行的,有BUG就是你自己的問題。
程式設計師:好的,聽你的!
JMM對編譯器說:你不能隨便改變程式設計師的程式碼順序,我都跟他承諾寫啥是啥了,別搞錯了。
編譯器:收到!(可我還是想優化,我不影響你不就行了,這優化我做定了,奧利給!)

於是就有了這些規則,而對於我們CRUD Boy來說都是不可見的,瞭解一下就OK!

感謝各位看官!

更多文章請掃碼關注或微信搜尋Java棧點公眾號!

公眾號二維碼

相關文章