自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Java內(nèi)存模型的深入分析

存儲 存儲軟件
曾經(jīng),計算機的世界遠沒有現(xiàn)在復雜,那時候的cpu只有單核,我們寫的程序也只會在單核上按代碼順序依次執(zhí)行,根本不用考慮太多。

[[355886]]

0x01 內(nèi)存模型產(chǎn)生的歷史背景

曾經(jīng),計算機的世界遠沒有現(xiàn)在復雜,那時候的cpu只有單核,我們寫的程序也只會在單核上按代碼順序依次執(zhí)行,根本不用考慮太多。

后來,隨著技術(shù)的發(fā)展,cpu的執(zhí)行速度和內(nèi)存的讀寫速度差異越來越大,人們很快發(fā)現(xiàn),如果還是按照代碼順序依次執(zhí)行的話,cpu會花費大量時間來等待內(nèi)存操作的完成,這造成了cpu的巨大浪費。

為了彌補cpu和內(nèi)存之間的速度差異,計算機世界的工程師們在cpu和內(nèi)存之間引入了緩存,雖然該方法極大的緩解了這一問題,但追求極致的工程師們覺得這還不夠,他們又想到了一個點子,就是通過合理調(diào)整內(nèi)存的讀寫順序來進一步緩解這個問題。

比如,在編譯時,我們可以把不必要的內(nèi)存讀寫去掉,把相關(guān)連的內(nèi)存讀寫盡量放到一起,充分利用緩存。

比如,在運行時,我們可以對內(nèi)存提前讀,或延遲寫,這樣使cpu不用總等待內(nèi)存操作的完成,充分利用cpu資源,避免計算能力的浪費。

這一想法的實施帶來了性能的巨大提升,但同時,它也帶來了一個問題,就是內(nèi)存讀寫的亂序,比如原本代碼中是先寫后讀,但在實際執(zhí)行時卻是先讀后寫,怎么辦呢?

為了避免內(nèi)存亂序給上層開發(fā)帶來困擾,這些工程師們又想到了可以通過分析代碼中的語義,把有依賴關(guān)系,有順序要求的代碼保持原有順序,把剩余的沒有依賴關(guān)系的代碼再進行性能優(yōu)化,亂序執(zhí)行,通過這樣的方式,就可以屏蔽底層的亂序行為,使代碼的執(zhí)行看起來還是和其編寫順序一樣,完美。

之后,計算機的性能就是在硬件的飛速發(fā)展及軟件執(zhí)行方式的深入優(yōu)化下,野蠻生長了很長一段時間,同樣一段代碼,即使什么都不改,每隔一段時間還是會有執(zhí)行速度上的成倍提升,這就是著名的摩爾定律時代。

但萬事皆有終結(jié),隨著硬件上遇到的物理瓶頸越來越多,軟件上對執(zhí)行方式的優(yōu)化已接近極限,單純通過單核來提升性能的方式顯示是已經(jīng)走不通了,此時,計算機世界已經(jīng)來到了一個十字路口,前方的路如何走,已經(jīng)成為困擾所有人的一個問題。

既然單核遇到了瓶頸,為什么不發(fā)展多核呢?

不知道誰想到了這個點子,一下引爆了整個計算機行業(yè),是啊,單核不行了我們可以發(fā)展多核嘛,自此,整個計算機行業(yè)都開始向多核轉(zhuǎn)變,也由此進入到了多核的大航海時代。

2005年,時任intel主席的Paul Otellini宣布,將所有的未來產(chǎn)品都轉(zhuǎn)向多核,并預測這將是歷史性的拐點。

事實也確實如此,到現(xiàn)在,我們用的所有的cpu基本都是多核。

多核時代的到來雖然重啟了計算機世界新一輪的發(fā)展,但也帶來了一個非常嚴峻的問題,那就是多核時代如何承接單核時代的歷史饋贈。

上面我們提到,在單核時代,為了追求代碼的極致性能,我們在編譯時和運行時都對內(nèi)存操作做了各種亂序處理,雖然已經(jīng)通過一定的方式,讓這種亂序不影響到上層開發(fā),對上層邏輯透明,但這種方式只對單核有效,進入到多核時代,單核運行不可見的亂序,在多核情況下都可見了,且此種亂序已經(jīng)嚴重影響到了多核代碼的正確編寫。

怎么辦呢?移除亂序優(yōu)化,讓代碼還是按照原來的順序方式執(zhí)行?

那這樣會造成巨大的性能損失,此種情況下由單核進化到多核的意義也就不大了,那怎么辦呢?

誰說代碼就一定要順序執(zhí)行的?誰說亂序就一定不能對上層可見的?我們可以好好看下我們寫的代碼,其實很多時候,我們并不介意亂序執(zhí)行,我們介意的只是某些關(guān)鍵位置上代碼的亂序執(zhí)行。

比如,一個線程先對n個普通變量進行寫操作,再對一個特殊的,用于標志寫完成的變量進行寫操作,此種情況下,我們并不關(guān)心前n次寫操作是否亂序,我們只要能確保前n次寫和最后一次寫之間是有序的就可以了,這樣,當其他線程要讀那前n次寫時,只要先讀最后那次寫,檢查其值是否表示前n次已經(jīng)寫完成,如果是,我們就可以放心的讀那前n次寫了。

默認亂序執(zhí)行,在關(guān)鍵節(jié)點保證有序,這種方式不僅使單核時代的各種亂序優(yōu)化依然有效,也使多核情況下的亂序行為有了一定的規(guī)范。

基于此,各種硬件平臺提供了自己的方式給上層開發(fā),約定好只要按我給出的方式編寫代碼,即使是在多核情況下,該保證有序的地方也一定會保證有序。

這套在多核情況下,依然可以讓開發(fā)者指定哪些代碼保證有序執(zhí)行的規(guī)則,就叫做內(nèi)存模型。

0x02 內(nèi)存模型的定義

內(nèi)存模型的英文是memory model,或者更精確的來說是memory consistency model,它其實就是一套方法或規(guī)則,用于描述如何在多核亂序的情況下,通過一定的方式,來保證指定代碼的有序執(zhí)行。

它是介于硬件和軟件之間,以一種協(xié)議的形式存在的。

對硬件來說,它描述的是硬件對外的行為規(guī)范,對軟件來說,它描述的是編寫多線程代碼的一套規(guī)則。

總之,內(nèi)存模型就相當于是在多核時代下,硬件的使用說明書,只要按照說明書來編寫多線程程序,那不管底層如何亂序,如何優(yōu)化,都不會影響你代碼的正確性。

0x03 Java的雄心壯志

上文我們提到,內(nèi)存模型描述的是在多核情況下,硬件的一套行為規(guī)范,也就是說,只要硬件支持多核,就必須提供一套自己的內(nèi)存模型。

這就衍生出了一個問題,就是不同硬件上的內(nèi)存模型差異很大,完全不兼容。

比如應用于桌面和服務器領(lǐng)域的x86平臺用的是x86 tso內(nèi)存模型。

比如應用于手機和平板等移動設備領(lǐng)域的arm平臺用的是weakly-ordered內(nèi)存模型。

比如最近幾年大火的riscv平臺用的是risc-v weak memory ordering內(nèi)存模型。

不同硬件平臺的內(nèi)存模型,描述的指令亂序情況,及禁止亂序的方式都完全不一樣,它們只適用于自己平臺的指令集,或者說只適用于自己平臺的匯編語言。

不過由于匯編語言本身就不具有跨平臺性,所以這樣做也無可厚非,甚至某種程度上來說,只能這樣做,基于這樣的事實,大家在各自的硬件平臺,按照對應的內(nèi)存模型規(guī)范,編寫著自己的匯編代碼,相安無事又其樂融融的享受著多核帶來的紅利。

如果只是寫匯編代碼的話,因為大家對其沒有什么跨平臺的預期,所以也沒啥太大的意見,但對于追求跨平臺的c/c++等高級語言來說,這個就有點不一樣了,由于其語言層面并沒有對多線程進行內(nèi)置支持,所以當用其寫的多線程代碼時,可能在一個平臺上是正確的,但到了另一個平臺就錯誤的。

這種局面終于在Java身上得到了改善。

1991年,James Gosling在Sun Microsystems發(fā)起了Java語言項目。

1996年,Sun Microsystems對外發(fā)布了Java 1.0版本。

由于Java的目標是write once, run anywhere,所以它不僅創(chuàng)造性的提出了字節(jié)碼中間層,讓字節(jié)碼運行在虛擬機上,而不是直接運行在物理硬件上,它還在語言層面內(nèi)置了對多線程的跨平臺支持,也為此提出了Java語言的內(nèi)存模型,這樣,當我們用Java寫多線程項目時,只要按照Java的內(nèi)存模型規(guī)范來編寫代碼,Java虛擬機就能保證我們的代碼在所有平臺上都是正確執(zhí)行的。

在語言層面支持多線程在現(xiàn)在看來不算什么,但在那個年代,這也算是一項大膽的創(chuàng)舉了,它也成為了首個主流編程語言中,內(nèi)置支持多線程編碼的語言。

但事與愿違,Java的先行者們大大低估了這項工作的復雜程度,以至于第一版的Java內(nèi)存模型有很多嚴重的問題,嚴重到一般人很難使用Java來編寫正確的多線程代碼。

經(jīng)過了很長一段時間的詬病,Java的設計者們終于決定重新修訂Java的內(nèi)存模型,這就是著名的JSR 133計劃,該計劃在2001年發(fā)起,經(jīng)過了長達3年的激烈討論,終于在2004年完成,并與Java 5.0一起對外發(fā)布。

至此,Java終于在語言層面正確的對線程跨平臺進行了內(nèi)置支持,并且也對各種硬件平臺的內(nèi)存模型進行了合理的抽象,形成一套自己的內(nèi)存模型。

0x04 Java內(nèi)存模型的影響

在Java從語言層面提煉出跨平臺的內(nèi)存模型獲得巨大成功之后,c和c++紛紛開始效仿借鑒,在Java內(nèi)存模型的基礎上,總結(jié)并改進出了適合自己語言的內(nèi)存模型,并在c11和c++11標準中對外發(fā)布。

自此之后的很多新興的編程語言,都會內(nèi)置對多線程的支持,且都會有自己的一套內(nèi)存模型。

0x05 Java內(nèi)存模型規(guī)范

完整的Java內(nèi)存模型是很復雜的,要不然JSR 133也不會花費三年時間才定下來,但如果我們只是想寫正確的多線程程序,它又非常簡單。

它的核心要義其實就一句話:

If a program is correctly synchronized, then all executions of the program will appear to be sequentially consistent.

為了追求語義上的精確,上面的話我直接引自Java Language Specification(JLS)。

它的大意就是說,如果我們的程序是正確同步的,則該程序在執(zhí)行時,不管底層如何亂序,它都會表現(xiàn)的像按代碼編寫順序那樣依次執(zhí)行。

這里我們要注意三點,一是correctly synchronized,二是appear to be,三是sequentially consistent。

首先,sequentially consistent指的是順序執(zhí)行,它給我們兩個保證,一是有序性,即代碼的執(zhí)行順序就像代碼本身寫的一樣,沒有亂序發(fā)生,如果是多線程在執(zhí)行多段代碼,那總的執(zhí)行順序就是多段代碼之間的穿插執(zhí)行。

二是可見性,即一行代碼執(zhí)行完畢后,比如寫內(nèi)存,下一行代碼對上一行代碼的執(zhí)行結(jié)果立即可見,比如讀到上一行代碼寫的值。

其實sequentially consistent本質(zhì)上就是我們正常人理解的代碼執(zhí)行順序,順序執(zhí)行、沒有亂序、讀寫立即可見、多線程代碼段穿插執(zhí)行。

上面那句話要注意的第二點是appear to be,即它只是讓我們看起來像在以sequentially consistent的方式在執(zhí)行,但真實的執(zhí)行順序很有可能還是在亂序,只是我們感知不到而已。

這個非常類似于單核時代的代碼執(zhí)行,它也是讓我們感覺代碼是在以sequentially consistent的方式在執(zhí)行,但底層其實是各種亂序的。

上面那句話中,最后一點我們需要注意的就是correctly synchronized,即正確同步的,那什么叫正確同步的呢,這個在JLS里也給了官方定義:

A program is correctly synchronized if and only if all sequentially consistent executions are free of data races.

它的大意是說,如果一個程序用各種假定的順序執(zhí)行方式來執(zhí)行其代碼時,都沒有發(fā)現(xiàn)數(shù)據(jù)競爭,那它就是correctly synchronized。

這里需要注意的是所有的sequentially consistent的執(zhí)行,因為程序中一般都存在大量的邏輯分支,一次執(zhí)行只會走分支的一個方向,假定我們把程序中的所有分支的所有方向都按代碼順序方式模擬執(zhí)行了一遍,還是沒有發(fā)現(xiàn)數(shù)據(jù)競爭,那這個程序就是correctly synchronized的。

為了方便記憶,我們把上面兩句話合并并精簡一下,大意為,如果我們的程序沒有data race,那它在被執(zhí)行時,其執(zhí)行順序我們就可以認為是代碼的編寫順序。

那何為data race呢,繼續(xù)看官方定義:

When a program contains two conflicting accesses that are not ordered by a happens-before relationship, it is said to contain a data race.

根據(jù)該定義我們可知,如果兩個accesses是conflicting的,且沒有被happens-before規(guī)則約束,則稱之為data race。

繼續(xù)來看下什么是conflicting accesses:

Two accesses to (reads of or writes to) the same variable are said to be conflicting if at least one of the accesses is a write.

其大意是說,對同一變量的兩個訪問操作,如果有一個是寫操縱,則這兩個訪問操作被成為conflicting的。

到這里,Java內(nèi)存模型的核心要義就快浮出水面了,我們把上面四句官方定義再合并精簡下,它說的其實就是:

如果程序中存在對同一變量的多個訪問操作,且至少有一個是寫操作,則這些訪問操作被稱為是conflicting操作,如果這些conflicting操作沒有被happens-before規(guī)則約束,則這些操作被稱為data race,有data race的程序就不是correctly synchronized,運行時也就無法保證sequentially consistent特性,沒有data race的程序就是correctly synchronized,運行時可保證sequentially consistent特性。

那也就是說,我們現(xiàn)在只要知道什么是happens-before規(guī)則,然后用這個規(guī)則約束程序中的所有conflicting操作,那我們最終的程序就是correctly synchronized,它在運行時讓我們感知到的順序就是代碼的編寫順序。

下面我們來看下什么是happens-before規(guī)則,為了精確定義,我們還是直接引用官方文檔:

Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second.

If we have two actions x and y, we write hb(x, y) to indicate that x happens-before y.

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

If an action x synchronizes-with a following action y, then we also have hb(x, y).

If hb(x, y) and hb(y, z), then hb(x, z).

第一句話是說,如果兩個行為之間有happens-before順序,則第一個行為一定發(fā)生在第二個行為之前,且第一個行為的結(jié)果對第二個行為可見。

理解這句話非常重要,它告訴我們,有happens-before關(guān)系的兩個行為之間,是能保證有序性和可見性的,或者說我們也可以理解為,它們之間不會發(fā)生亂序。

第二句話是說用hb(x, y)形式表示x發(fā)生在y之前。

第三句話是說,同一線程中,如果x的出現(xiàn)順序是在y之前,則x發(fā)生在y之前。

第四句話是說,如果x synchronizes-with y,則我們說x發(fā)生在y之前。

第五句話是說,happens-before規(guī)則有傳遞性,這點非常重要,它也是經(jīng)常被忽視的一點。

從上面的這段話我們可以知道,happens-before規(guī)則由兩部分組成,一部分是program order,即單線程中代碼的編寫順序,另一部分是synchronizes-with,即多線程中的各種同步原語。

也就是說,在單線程中,代碼編寫的前后順序之間有happens-before關(guān)系,在多線程中,有synchronizes-with關(guān)聯(lián)的代碼之間也有happens-before關(guān)系。

program order非常好理解,這里我們就不再說了,下面我們來重點看下synchronizes-with,還是直接引自JLS:

An unlock action on monitor m synchronizes-with all subsequent lock actions on m (where "subsequent" is defined according to the synchronization order).

A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).

An action that starts a thread synchronizes-with the first action in the thread it starts.

The write of the default value (zero, false, or null) to each variable synchronizes-with the first action in every thread.

The final action in a thread T1 synchronizes-with any action in another thread T2 that detects that T1 has terminated.

T2 may accomplish this by calling T1.isAlive() or T1.join().

If thread T1 interrupts thread T2, the interrupt by T1 synchronizes-with any point where any other thread (including T2) determines that T2 has been interrupted (by having an InterruptedException thrown or by invoking Thread.interrupted or Thread.isInterrupted).

因為synchronizes-with規(guī)則比較多,我們就不逐行翻譯了,它們講的都是Java中的同步原語的一些使用,比如我們常用的synchronized關(guān)鍵字,volatile關(guān)鍵字等。

知道這些關(guān)系后,我們再來總結(jié)下,happens-before規(guī)則包括兩種,一種是program order,是定義單線程內(nèi)各種操作的順序,另一種是synchronizes-with,是定義多線程之間操作的順序。

到現(xiàn)在為止,我們就清晰的知道如何寫一個correctly synchronized的程序了,找到所有的conflicting操作,然后用上述happens-before規(guī)則進行約束,那該程序就是correctly synchronized的了。

correctly synchronized的程序雖然不能保證我們代碼邏輯上的正確性,但因為其提供的sequentially consistent特性,我們至少可以很容易的,且有根據(jù)的分析我們的代碼是否正確了。

如果不是correctly synchronized的程序,也就沒有sequentially consistent特性,那在運行時,很多地方的代碼都可能是亂序的,我們也就無從分析代碼的正確性了。

好,如果你只是想寫正確的多線程程序,Java內(nèi)存模型理解到這個程度已經(jīng)算是可以了,但如果你想了解更多細節(jié),可以繼續(xù)往下看。

在JLS規(guī)范的第17章 Threads and Locks 里完整定義了Java內(nèi)存模型的各種規(guī)則,上面引用的各種官方定義也都是從這里來的。

該章中最難理解的,或者說根本無法理解的其實是其causality語義,那什么是causality呢?

上文中我們講的都是如何正確的寫一個correctly synchronized的程序,以便我們能在程序執(zhí)行時得到sequentially consistent保證。

那如果我們寫的程序不是correctly synchronzed,或者說代碼里有各種data race,會發(fā)生什么呢,結(jié)果會是什么樣子呢?

如果讀過類似于c/c++標準規(guī)范文檔的同學一定知道,如果是在那里定義非correctly synchronized的執(zhí)行結(jié)果,那一定是undefined,即未定義,任何情況都可能發(fā)生。

也就是說,類似于c/c++規(guī)范里,它們只定義正確行為,如果你不按照正確行為寫,那后果自負。

但是Java既然是以安全著稱的語言,它是沒法容忍所有后果都自負的,即使我們寫的程序不是correctly synchronzed的。

所以causality定義的是,即使我們寫的程序不是correctly synchronzed的,那有些極端的結(jié)果也是不能發(fā)生的,這樣保證了即使出現(xiàn)了壞的結(jié)果,也不至于壞到我們接受不了的程度。

限于篇幅及復雜度的原因,causality相關(guān)的東西我們就不展開講了,如果真對這些感興趣,可以看本文結(jié)尾的參考資料,或私聊我單獨討論。

Java內(nèi)存模型中還有一部分是講final關(guān)鍵字的,因為這個是獨立的,且在JLS中講的比較清楚,這里就不再展開講了,還是像上面說的,有問題可以私聊我,我們一起討論。

0x06 實戰(zhàn)看看

假設方法f1, f2分別被兩個線程執(zhí)行,且f1的線程先獲得鎖,則f2中r1, r2讀到的值分別是什么?

我們上面講happens-before中的synchronizes-with規(guī)則時提到過,unlock操作發(fā)生在同一個monitor上后續(xù)的lock操作之前,對比上面的代碼我們可知,操作4發(fā)生在操作5之前。

又根據(jù)happens-before中的program order規(guī)則我們可知,操作3發(fā)生在操作4之前,操作5發(fā)生在操作6之前。

上面我們還提到過,happens-before規(guī)則不僅保證有序性,還保證可見性,即happens-before之前的操作對happens-before之后的操作可見。

綜上可知r1的值一定是1。

r1的值為1這個結(jié)果,即使不用happens-before規(guī)則論證,對于絕大部分學過java的人來說也是很好理解的,那r2的值是多少呢?

對,也一定是1。

同樣根據(jù)happens-before中的program order規(guī)則可知,方法f1中的happens-before順序是 1 -> 2 -> 3 -> 4,方法f2中的happens-before順序是 5 -> 6 -> 7 -> 8。

又根據(jù)happens-before中的synchronizes-with規(guī)則可知,操作4發(fā)生在操作5之前。

我們上面還提到過,happens-before規(guī)則是有傳遞性的,所以方法f1和f2整個happens-before順序為 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8,又因為happens-before規(guī)則保證可見性,所以操作1中寫入的值,在操作8中一定可見,所以r2的值一定是1。

這里我重點想要強調(diào)的是,操作1對操作8可見,這個也是很多人會忽視的。

我們在學synchronized關(guān)鍵字時,講的最多的就是它的原子性,我獲得了鎖別人就沒法獲得,所以在獲得鎖期間的操作都是單線程的,都是原子的,但是卻很少有人會去講它的有序性和可見性。

synchronized關(guān)鍵字的解鎖操作,是發(fā)生在后續(xù)的加鎖操作之前的,它們之間是有happens-before關(guān)系的,所以synchronized解鎖之前的操作,對synchronized加鎖之后的操作是有序并可見的。

這里的操作不僅包括synchronized里的操作,還包括對應的synchronized之前或之后的操作。

由這個例子我們也可以看到,在單線程中通過program order定義happens-before關(guān)系,在跨線程中通過synchronizes-with定義happens-before關(guān)系,兩個結(jié)合,就建立起了一條完整的,跨線程的happens-before關(guān)系,從而也就能確立了跨線程之間操作的的有序性及可見性。

好,我們還是用上面的代碼,假設方法f2的線程先獲得鎖,方法f1的線程后獲得鎖,那r1和r2的值分別是什么?

同樣根據(jù)我們上面討論過的happens-before規(guī)則可知,操作7是發(fā)生在操作2之前的,操作1和操作8之間沒有任何happens-before關(guān)系,所以,r1的值一定是0,而r2的值無法確定。

其實這個程序就是典型的沒有correctly synchronized的程序,因為操作1和操作8這兩個conflicting操作,并沒有被happens-before規(guī)則約束,所以該程序出現(xiàn)了data race,所以該程序不是correctly synchronized,這是我們平時要避免的情況。

我們再看一個例子:

假設方法f1,f2分別被兩個線程獨立執(zhí)行,那r1,r2的值分別是多少?

先說答案,r1的值一定是1,r2的值無法確定。

為什么呢?

首先,根據(jù)happens-before中的program order規(guī)則,操作1,2,3之間是順序執(zhí)行,操作4,5,6之間也是順序執(zhí)行,又根據(jù)happens-before中synchronizes-with規(guī)則的,volatile的寫操作發(fā)生在后續(xù)的volatile讀操作之前,所以可知,3和4之間也有happens-before關(guān)系。

那按這樣來看的話,r1,r2的值都應該是1啊,因為3,4之間建立了跨線程的執(zhí)行順序,而各自線程中又按照代碼的順序執(zhí)行,為什么r1就一定是1,r2就無法確定呢?

這又是一個典型的對volatile理解深度的問題。

對volatile的理解深度可分為三層:

第一層是絕大部分資料都會告訴你的,volatile能保證跨線程間的可見性,這是最基礎的理解。

第二層的理解就比較少的資料會提到,就是有序性,可能有些資料只是提到了有序性,但又沒有說明誰和誰有序。

volatile保證的有序性其實是在跨線程之間建立了一條happens-before規(guī)則,即volatile的寫操作發(fā)生在后續(xù)的volatile讀操作之前,它只建立了這一條有序關(guān)系,而volatile之前或之后的有序關(guān)系,比如上面例子中的操作1,2和操作5,6是通過program order建立的。

所以說volatile保證的有序是幫助串聯(lián)起跨線程之間操作的有序。

這就是第二層的理解,能說到這層且能說明白的資料其實已經(jīng)比較少了,至于能說到第三層的就更少了。

在說第三層的理解之前,我們再來看下volatile建立的happens-before規(guī)則是,volatile的寫操作是發(fā)生在后續(xù)的讀操作之前。

理解第三層的關(guān)鍵,是你如何理解 后續(xù)的 這三個字。

何為后續(xù)?在代碼中我們只有讀到了volatile的寫操作的那個值,我們才能確定這些讀操作是寫操作的后續(xù)操作,比如上面代碼的操作4中,只有我們讀到了true,我們才能確定這次讀是上面volatile寫的后續(xù)操作,這樣我們才能確立volatile給我們保證的happens-before順序。

這也是為什么r1一定能讀到1,而r2值無法確定的原因,因為操作6雖然發(fā)生在操作4之后,但如果操作4沒有讀到true,則操作3和操作4之間的happens-before關(guān)系還是沒法建立的,也就是說,操作2和操作6之間的happens-before關(guān)系無法確立。

理解到這一層,才算是真正理解volatile。

再來看一個例子:

這次還是假設方法f1,f2分別被兩個線程執(zhí)行,且f1先執(zhí)行,f2后執(zhí)行,那f2返回的值一定是1嗎?

不一定,因為要想保證可見性,必須要確立happens-before關(guān)系,而跨線程的happens-before關(guān)系的確立只有上面我們提到的那幾種,比如解鎖和后續(xù)的加鎖操作,比如volatile寫和后續(xù)的volatile讀操作,比如有關(guān)線程啟動或終止的一些操作等。

但我們看上面的方法,雖然f1有synchronized的解鎖操作,但f2并沒有對應的加鎖操作,所以f1和f2的線程之間沒有任何happens-before規(guī)則,也就無法保證它們之間讀寫的可見性。

再來看一個例子:

這次還是假設f1,f2被兩個不同線程執(zhí)行,且f1先執(zhí)行,f2后執(zhí)行,那f2返回值一定是1嗎?

不一定,因為雖然它們都有加解鎖操作,但它們是對不同的鎖,只有是對同樣的鎖的加解鎖之間才有happens-before關(guān)系,才能保證可見性。

雖然我還想寫更多的例子讓大家來了解java的內(nèi)存模型,但限于篇幅原因,就寫的到這里吧,其實萬變不離其宗,只要掌握了java內(nèi)存模型的核心要義,那所有的例子你都能正確的解答出來。

我們再來重新梳理下java內(nèi)存模型的核心要義:

要想寫正確的多線程代碼,我們必須要保證我們的代碼是correctly synchronized,也就是說,是沒有data race的。

而data race的產(chǎn)生,是因為我們沒有用happens-before規(guī)則限制代碼中的conflicting操作,即那些對同一變量存在多個訪問,且其中至少有一個操作是寫操作的那些操作。

也就是說,如果我們對所有conflicting操作都用happens-before規(guī)則限制,那我們的程序就是correctly synchronized的了,也就能保證運行時的sequentially consistent特性。

happens-before規(guī)則由兩部分組成,一部分是program order規(guī)則,即單線程中代碼的字面順序,另一部分是synchronizes-with規(guī)則,即各種同步操作,比如synchronized關(guān)鍵字,volatile關(guān)鍵字,線程的啟動關(guān)閉操作等。

想寫正確的多線程代碼,你只需要記住這些就好了。

當然,內(nèi)存模型中還有對final關(guān)鍵字正確使用的說明,這個因為是獨立出來的一部分,且比較好理解,這里就不再講了。

好,實戰(zhàn)部分的內(nèi)容就這些。

0x07 Java內(nèi)存模型的底層實現(xiàn)

原本這篇文章我還想詳細說下Java內(nèi)存模型在各硬件平臺上是怎么實現(xiàn)的,但沒想到寫到這里都已經(jīng)這么多字了,所以,有關(guān)實現(xiàn)的部分,我們還是改天令起一篇文章再寫。

如果對實現(xiàn)部分感興趣的同學可以關(guān)注我的公眾號:卯時卯刻,后者搜ytcode,有關(guān)實現(xiàn)部分的文章會首發(fā)在我的公眾號里。

但為了滿足一下大家的好奇心,我簡單提一下,在x86平臺上,volatile的讀操作沒有任何消耗,volatile的寫操作使用的是 lock 匯編指令,相關(guān)代碼為:

0x08 文章總結(jié)

文章開始我們說到,內(nèi)存模型的出現(xiàn),是因為單核到多核,或者說是,單線程到多線程時代過渡時,我們需要一種方式,來解決多線程內(nèi)存讀寫亂序的問題,而內(nèi)存模型正是一套這樣的規(guī)則,它定義了用什么樣的方式,可以保證多線程間相關(guān)代碼不會有亂序發(fā)生,即保證了相關(guān)代碼的有序性和可見性。

而又由于硬件平臺有非常多的種類,比如x86, arm, riscv等,每種平臺都有自己的一套內(nèi)存模型,這對于我們編寫跨平臺的代碼來說,非常麻煩。

為了實現(xiàn)write once, run anywhere的宏圖大志,Java作為當時的主流編程語言,第一個嘗試去從語言層面,抽象出對各物理平臺都適用的一套內(nèi)存模型。

雖然過程非??部?,第一次的嘗試失敗了,第二次的嘗試花了三年時間才最終定稿,但Java最終還是實現(xiàn)了自己的目標,首次實現(xiàn)了語言層面上的內(nèi)存模型。

這對后續(xù)的編程語言產(chǎn)生了深遠的影響,現(xiàn)在看看那些新興的編程語言,每種都內(nèi)置對多線程的跨平臺支持,每種都有自己的內(nèi)存模型規(guī)范。

壯哉 Java!

0x09 參考資料

[1] Java Language Specification 第17章 Threads and Locks

https://docs.oracle.com/javase/specs/index.html

[2] JSR 133

http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf

[3] http://rsim.cs.uiuc.edu/Pubs/popl05.pdf

[4] https://gpetri.github.io/publis/jmm-vamp07.pdf

[5] http://groups.inf.ed.ac.uk/request/jmmexamples.pdf

本文轉(zhuǎn)載自微信公眾號「卯時卯刻」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系卯時卯刻公眾號。

 

責任編輯:武曉燕 來源: 卯時卯刻
相關(guān)推薦

2023-02-01 08:13:30

Redis內(nèi)存碎片

2018-10-25 15:24:10

ThreadLocal內(nèi)存泄漏Java

2015-08-03 09:54:26

Java線程Java

2010-09-07 14:21:22

PPPoE協(xié)議

2022-04-12 08:30:45

TomcatWeb 應用Servlet

2011-03-23 11:01:55

LAMP 架構(gòu)

2014-10-30 15:08:21

快速排序編程算法

2010-03-08 14:53:48

Linux分區(qū)

2011-09-01 13:51:52

JavaScript

2022-08-30 07:00:18

執(zhí)行引擎Hotspot虛擬機

2021-10-29 16:36:53

AMSAndroidActivityMan

2009-12-14 14:50:46

Ruby傳參數(shù)

2009-12-16 16:39:01

Visual Stud

2009-06-10 18:12:38

Equinox動態(tài)化OSGi動態(tài)化

2010-09-29 15:52:15

2010-10-14 12:44:02

無線LAN故障處理

2018-12-18 10:11:37

軟件復雜度軟件系統(tǒng)軟件開發(fā)

2013-11-14 17:02:41

Android多窗口

2011-09-13 09:08:22

架構(gòu)

2023-08-07 07:44:44

點贊
收藏

51CTO技術(shù)棧公眾號