打工人!肝了這套多線程吧!壹


開篇閑扯

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

並發症

    並發問題,曾經在單核單線程的機器上是不存在的(不是不想,是做不到)。假如把計算機看成一個木桶,那么跟我們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棧點公眾號!

公眾號二維碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM