開篇閑扯
一年又一年,年年多線程。不論你是什么程序員,都逃脫不了多線程並發的魔爪。因為它從盤古開天辟地的時候就有了,就是在計算機中對現實世界的一種抽象。因此,放輕松別害怕,肝了這系列的多線程文章,差不多能吊打面試官了(可別真動手...)。
並發症
並發問題,曾經在單核單線程的機器上是不存在的(不是不想,是做不到)。假如把計算機看成一個木桶,那么跟我們Java開發人員關系最大的就是CPU、內存、IO設備。這三塊木板發展至今,彼此之間也形成了較大的性能差異。CPU的核心數線程數在不斷增多,內存的速度卻跟不上CPU的步伐,同理IO設備也沒能跟上內存的步伐。於是就加緩存,經過科學論證三級緩存最靠譜,於是就有了常見的CPU三級緩存。然后前輩們再對操作系統做各類調度層面的深度優化,通過軟硬兼施的手法,使得軟件與硬件的完美結合,才有如今繁榮的互聯網。而我們不過是在這座城市里的打工人罷了。
言歸正傳,本文將分別說明在並發世界里的“三宗罪”:可見性、原子性、有序性。
罪狀一:可見性
前文中有說到CPU的發展經歷了從單核單線程到現在的多核心多線程,而內存的讀寫性能卻供應不上CPU的處理能力,於是就增加了緩存,至於前文中提到的三級緩存為什么是三級,不在本文討論范圍,有興趣自己看去。。。
為什么會有可見性問題?
在單核心時代,所有的線程都是交給一個CPU串行執行,因此不論有多少線程都是排隊執行,也就不會形成線程A與B同時競爭target變量的競爭狀態,如圖一。
來到多核心多線程時代,每顆CPU都有各自的緩存,如果多個線程分別在不同的CPU上運行,且需要同時操作同一個數據。而每顆CPU在處理內存中的數據時,會先將目標數據緩存到CPU緩存中。這時候CPU們各干各的,也不管目標值有沒有被其他CPU修改過,自己在緩存中修改后不管三七二十一就寫回去,這肯定是不行的啊兄弟...,而這就是我們Java中常說的數據可見性問題,再追根溯源就是:CPU級別的緩存一致性協議。后邊文章會詳細解釋(別問具體時間,問了就是明天)。
可見性問題怎么解決?
這個簡單,如果僅僅是解決可見性,那就Volatile關鍵字用起來(也不是萬能的,慎重考慮),它會將共享變量數據從線程工作內存刷新到主存中,而這個關鍵字的實現基礎是Java規范的內存模型,注意,這里要和JVM內存模型區分開,兩者是不一樣的東西。那么Java內存模型又是什么樣的,為啥設計這個內存模型,有哪些好處?下篇詳細解釋!本文就先放一張簡單的圖:
罪狀二:原子性
大家都知道CPU的運行時間是分片進行的,可能CPU這段時間在執行我寫的if-else,下一時刻由於操作系統的調度當前線程丟失時間片,又執行其他線程任務去了(呸!渣男)。打斷了我當前線程的一個或者多個操作流程,這就是原子性被破壞了,也就是多線程無鎖情況下的ABA問題。跟我們期望的完全不一樣啊,還是看圖說話:
解釋一下就是:想要得到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棧點公眾號!