什么是java內存模型?
導致可見性的原因是緩存,導致有序性的原因是編譯優(yōu)化,那解決可見性,有序性最直接的辦法就是禁用緩存和編譯優(yōu)化,但是這樣問雖然解決了,我們程序的性能可就堪憂了.
合理的方案應該是按需禁用緩存以及編譯優(yōu)化,那么怎么做到按需禁用呢?對于并發(fā)程序,何時禁用緩存以及編譯優(yōu)化只有程序員知道,那所謂的"按需禁用"其實就是指按照程序員的要求來禁用,所以,為了解決可見性和有序性問題,只需要提供程序員按需禁用緩存和編譯優(yōu)化的方法即可.
java內存模型是個很復雜的規(guī)范,可以從不同的視角來解讀,站在我們這些程序員的視角,本質上可以理解為,java內存模型規(guī)范了JVM如何提供按需禁用和編譯優(yōu)化的方法.具體來說,這些方法包括volatile.synchronized和final三個關鍵字,以及六項Happens-Before規(guī)則,這也是重點.
使用volatile的困惑
volatile關鍵字并不是java語言的特產,古老的C語言里也有,它最原始的意義就是禁用CPU緩存.
例如,聲明一個volatile變量volatile int x = 0,它表達的是:告訴編譯器,對這個變量的讀寫,不能使用CPU緩存,必須從內存中讀取或者寫入.這個語義看上去相當明確,但是實際使用的時候卻會帶來困惑.
例如下面的代碼,假設線程A執(zhí)行writer()方法,按照volatile語義,會把變量"v = true" 寫入內存;假設線程B執(zhí)行reader()方法,同樣按照volatile語義,線程B會從內存中讀取變量v,如果線程B看到"v=true"時,那么線程B看到的變量x是多少?
直覺上,應該是42,但實際要看java版本,1.5之前x可能是42,也可能是0; 1.5以上版本運行x就是42.
分析一下,1.5之前版本出現(xiàn)x=0情況變量x可能被CPU緩存而導致可見性問題.這個問題在1.5版本已經被解決,java內存模型在1.5版本對volatile語義進行了增強.答案就是一項Happens-Before規(guī)則.
Happens-Before 規(guī)則
Happens-Before 并不是說前面一個操作發(fā)生在后續(xù)操作的前面,它真正要表達的是:前面一個操作的結果對后續(xù)操作是可見的茸俭。
比較正式的說法是:Happens-Before 約束了編譯器的優(yōu)化行為,雖允許編譯器優(yōu)化,但是要求編譯器優(yōu)化后一定遵守 Happens?Before 規(guī)則.
1. 程序的順序性規(guī)則
這條規(guī)則是指在一個線程中,按照程序順序,前面的操作Happens-Before于后續(xù)的任意操作.這還是比較容易理解的.比如剛才的代碼.按照程序的順序,第6行代碼"x=42"Happens-Before于第7行代碼"v=true",這就是規(guī)則1的內容,也比較符合單線程里面的思維:程序前面對某個變量的修改一定是對后續(xù)操作可見的.
2. volatile 變量規(guī)則
這條規(guī)則是指對一個volatile變量的寫操作,Happens-Before于后續(xù)對這個volatile變量的讀操作.
這個就有點費解了,對一個volatile變量的寫操作相對于后續(xù)對這個volatile變量的讀操作可見,這怎么看都是禁用緩存的意思啊.貌似和1.5版本之前的語義沒有變化?如果關聯(lián)規(guī)則3就有不一樣的感覺了!
3.傳遞性
這條規(guī)則如果A Happens-Before B,且B Happens-Before C 那么 A Happens-Before C.
應用到代碼中:
從圖中看出:
1.x=42 Happens-Before 寫變量v = true ,這是規(guī)則1.
2.寫變量v = true?Happens-Before 讀變量 v = true,這是規(guī)則2.
根據(jù)傳遞性,x = 42?Happens-Before 讀變量v = true.意味者如果線程B督導了v = true,那么線程A設置的x = 42 對線程B是可見的,也就是說,線程B能看到x == 42.這就是1.5版本對volatile語義的增強,1.5 版本的并發(fā)工具包(java.util.concurrent)就是靠 volatile 語義來搞定可見性的.
4. 管程中鎖的規(guī)則
這條規(guī)則是指對一個鎖的解鎖 Happens-Before 于后續(xù)對這個鎖的加鎖镶殷。
管程是一種通用的同步原語利花,在Java 中指的就是 synchronized聪全,synchronized 是 Java 里對管程的實現(xiàn)捶惜。
管程中的鎖在 Java 里是隱式實現(xiàn)的,例如下面的代碼荔烧,在進入同步塊之前,會自動加鎖汽久,而在代碼塊執(zhí)行完會自動釋放鎖鹤竭,加鎖以及釋放鎖都是編譯器幫我們實現(xiàn)的.
假設 x 的初始值是 10,線程 A 執(zhí)行完代碼塊后 x 的值會變成 12(執(zhí)行完自動釋放鎖)景醇,線程 B 進入代碼塊時臀稚,能夠看到線程 A 對 x 的寫操作,也就是線程 B 能夠看到 x==12三痰。
5. 線程 start() 規(guī)則
這條是關于線程啟動的吧寺。它是指主線程 A 啟動子線程 B 后,子線程 B 能夠看到主線程在啟動子線程 B 前的操作散劫。
換句話說就是稚机,如果線程 A 調用線程 B 的 start() 方法(即在線程 A 中啟動線程 B),那么該 start() 操作 Happens-Before 于線程 B 中的任意操作获搏。
6. 線程 join() 規(guī)則
這條是關于線程等待的.它指的是主線程A等待子線程B完成(主線程A通過調用子線程B的join()方法實現(xiàn)),但子線程B完成后(主線程A中join()方法返回),主線程能夠看到子線程的操作,看到指的是對共享變量的操作.
換句話說就是赖条,如果在線程 A 中,調用線程 B 的 join() 并成功返回常熙,那么線程 B 中的任意操作 Happens-Before 于該 join() 操作的返回纬乍。
被我們忽視的 final
volatile 為的是禁用緩存以及編譯優(yōu)化,
final 修飾變量時裸卫,初衷是告訴編譯器:這個變量生而不變仿贬,可以可勁兒優(yōu)化。Java 編譯器在 1.5 以前的版本的確優(yōu)化得很努力墓贿,以至于都優(yōu)化錯了茧泪。問題類似于上一期提到的利用雙重檢查方法創(chuàng)建單例蜓氨,構造函數(shù)的錯誤重排導致線程可能看到 final 變量的值會變化
在 1.5 以后 Java 內存模型對 final 類型變量的重排進行了約束
總結
在 Java 語言里面,Happens-Before 的語義本質上是一種可見性调炬,A Happens-Before B意味著 A 事件對 B 事件來說是可見的语盈,無論 A 事件和 B 事件是否發(fā)生在同一個線程里。例如 A 事件發(fā)生在線程 1 上缰泡,B 事件發(fā)生在線程 2 上刀荒,Happens-Before 規(guī)則保證線程 2上也能看到 A 事件的發(fā)生。