java在线编译器_什么是Java内存模型
在知識星球中,有個小伙伴提了一個問題:有一個關于JVM名詞定義的問題,說”JVM內(nèi)存模型“,有人會說是關于JVM內(nèi)存分布(堆棧,方法區(qū)等)這些介紹,也有地方說(深入理解JVM虛擬機)上說Java內(nèi)存模型是JVM的抽象模型(主內(nèi)存,本地內(nèi)存)。這兩個到底怎么區(qū)分啊?有必然關系嗎?比如主內(nèi)存就是堆,本地內(nèi)存就是棧,這種說法對嗎?
時間久了,我也把內(nèi)存模型和內(nèi)存結(jié)構(gòu)給搞混了,所以抽了時間把JSR133規(guī)范中關于內(nèi)存模型的部分重新看了下。
后來聽了好多人反饋:在面試的時候,有面試官會讓你解釋一下Java的內(nèi)存模型,有些人解釋對了,結(jié)果面試官說不對,應該是堆啊、棧啊、方法區(qū)什么的(這不是半吊子面試么,自己概念都不清楚)
JVM中的堆啊、棧啊、方法區(qū)什么的,是Java虛擬機的內(nèi)存結(jié)構(gòu),Java程序啟動后,會初始化這些內(nèi)存的數(shù)據(jù)。
內(nèi)存結(jié)構(gòu)就是上圖中內(nèi)存空間這些東西,而Java內(nèi)存模型,完全是另外的一個東西。
什么是內(nèi)存模型
在多CPU的系統(tǒng)中,每個CPU都有多級緩存,一般分為L1、L2、L3緩存,因為這些緩存的存在,提供了數(shù)據(jù)的訪問性能,也減輕了數(shù)據(jù)總線上數(shù)據(jù)傳輸?shù)膲毫?#xff0c;同時也帶來了很多新的挑戰(zhàn),比如兩個CPU同時去操作同一個內(nèi)存地址,會發(fā)生什么?在什么條件下,它們可以看到相同的結(jié)果?這些都是需要解決的。
所以在CPU的層面,內(nèi)存模型定義了一個充分必要條件,保證其它CPU的寫入動作對該CPU是可見的,而且該CPU的寫入動作對其它CPU也是可見的,那這種可見性,應該如何實現(xiàn)呢?
有些處理器提供了強內(nèi)存模型,所有CPU在任何時候都能看到內(nèi)存中任意位置相同的值,這種完全是硬件提供的支持。
其它處理器,提供了弱內(nèi)存模型,需要執(zhí)行一些特殊指令(就是經(jīng)常看到或者聽到的,memory barriers內(nèi)存屏障),刷新CPU緩存的數(shù)據(jù)到內(nèi)存中,保證這個寫操作能夠被其它CPU可見,或者將CPU緩存的數(shù)據(jù)設置為無效狀態(tài),保證其它CPU的寫操作對本CPU可見。通常這些內(nèi)存屏障的行為由底層實現(xiàn),對于上層語言的程序員來說是透明的(不需要太關心具體的內(nèi)存屏障如何實現(xiàn))。
前面說到的內(nèi)存屏障,除了實現(xiàn)CPU之前的數(shù)據(jù)可見性之外,還有一個重要的職責,可以禁止指令的重排序。
這里說的重排序可以發(fā)生在好幾個地方:編譯器、運行時、JIT等,比如編譯器會覺得把一個變量的寫操作放在最后會更有效率,編譯后,這個指令就在最后了(前提是只要不改變程序的語義,編譯器、執(zhí)行器就可以這樣自由的隨意優(yōu)化),一旦編譯器對某個變量的寫操作進行優(yōu)化(放到最后),那么在執(zhí)行之前,另一個線程將不會看到這個執(zhí)行結(jié)果。
當然了,寫入動作可能被移到后面,那也有可能被挪到了前面,這樣的“優(yōu)化”有什么影響呢?這種情況下,其它線程可能會在程序?qū)崿F(xiàn)“發(fā)生”之前,看到這個寫入動作(這里怎么理解,指令已經(jīng)執(zhí)行了,但是在代碼層面還沒執(zhí)行到)。通過內(nèi)存屏障的功能,我們可以禁止一些不必要、或者會帶來負面影響的重排序優(yōu)化,在內(nèi)存模型的范圍內(nèi),實現(xiàn)更高的性能,同時保證程序的正確性。
下面看一個重排序的例子:
Class Reordering { int x = 0, y = 0; public void writer() { x = 1; y = 2; } public void reader() { int r1 = y; int r2 = x; }}假設這段代碼有2個線程并發(fā)執(zhí)行,線程A執(zhí)行writer方法,線程B執(zhí)行reader方法,線程B看到y(tǒng)的值為2,因為把y設置成2發(fā)生在變量x的寫入之后(代碼層面),所以能斷定線程B這時看到的x就是1嗎?
當然不行! 因為在writer方法中,可能發(fā)生了重排序,y的寫入動作可能發(fā)在x寫入之前,這種情況下,線程B就有可能看到x的值還是0。
在Java內(nèi)存模型中,描述了在多線程代碼中,哪些行為是正確的、合法的,以及多線程之間如何進行通信,代碼中變量的讀寫行為如何反應到內(nèi)存、CPU緩存的底層細節(jié)。
在Java中包含了幾個關鍵字:volatile、final和synchronized,幫助程序員把代碼中的并發(fā)需求描述給編譯器。Java內(nèi)存模型中定義了它們的行為,確保正確同步的Java代碼在所有的處理器架構(gòu)上都能正確執(zhí)行。
synchronization 可以實現(xiàn)什么
Synchronization有多種語義,其中最容易理解的是互斥,對于一個monitor對象,只能夠被一個線程持有,意味著一旦有線程進入了同步代碼塊,那么其它線程就不能進入直到第一個進入的線程退出代碼塊(這因為都能理解)。
但是更多的時候,使用synchronization并非單單互斥功能,Synchronization保證了線程在同步塊之前或者期間寫入動作,對于后續(xù)進入該代碼塊的線程是可見的(又是可見性,不過這里需要注意是對同一個monitor對象而言)。在一個線程退出同步塊時,線程釋放monitor對象,它的作用是把CPU緩存數(shù)據(jù)(本地緩存數(shù)據(jù))刷新到主內(nèi)存中,從而實現(xiàn)該線程的行為可以被其它線程看到。在其它線程進入到該代碼塊時,需要獲得monitor對象,它在作用是使CPU緩存失效,從而使變量從主內(nèi)存中重新加載,然后就可以看到之前線程對該變量的修改。
但從緩存的角度看,似乎這個問題只會影響多處理器的機器,對于單核來說沒什么問題,但是別忘了,它還有一個語義是禁止指令的重排序,對于編譯器來說,同步塊中的代碼不會移動到獲取和釋放monitor外面。
下面這種代碼,千萬不要寫,會讓人笑掉大牙:
synchronized (new Object()) {}這實際上是沒有操作的操作,編譯器完成可以刪除這個同步語義,因為編譯知道沒有其它線程會在同一個monitor對象上同步。
所以,請注意:對于兩個線程來說,在相同的monitor對象上同步是很重要的,以便正確的設置happens-before關系。
final 可以影響什么
如果一個類包含final字段,且在構(gòu)造函數(shù)中初始化,那么正確的構(gòu)造一個對象后,final字段被設置后對于其它線程是可見的。
這里所說的正確構(gòu)造對象,意思是在對象的構(gòu)造過程中,不允許對該對象進行引用,不然的話,可能存在其它線程在對象還沒構(gòu)造完成時就對該對象進行訪問,造成不必要的麻煩。
class FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; } static void writer() { f = new FinalFieldExample(); } static void reader() { if (f != null) { int i = f.x; int j = f.y; } }}上面這個例子描述了應該如何使用final字段,一個線程A執(zhí)行reader方法,如果f已經(jīng)在線程B初始化好,那么可以確保線程A看到x值是3,因為它是final修飾的,而不能確保看到y(tǒng)的值是4。如果構(gòu)造函數(shù)是下面這樣的:
public FinalFieldExample() { // bad! x = 3; y = 4; // bad construction - allowing this to escape global.obj = this;}這樣通過global.obj拿到對象后,并不能保證x的值是3.
volatile可以做什么
Volatile字段主要用于線程之間進行通信,volatile字段的每次讀行為都能看到其它線程最后一次對該字段的寫行為,通過它就可以避免拿到緩存中陳舊數(shù)據(jù)。它們必須保證在被寫入之后,會被刷新到主內(nèi)存中,這樣就可以立即對其它線程可以見。類似的,在讀取volatile字段之前,緩存必須是無效的,以保證每次拿到的都是主內(nèi)存的值,都是最新的值。volatile的內(nèi)存語義和sychronize獲取和釋放monitor的實現(xiàn)目的是差不多的。
對于重新排序,volatile也有額外的限制。
下面看一個例子:
class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { //uses x - guaranteed to see 42. } }}同樣的,假設一個線程A執(zhí)行writer,另一個線程B執(zhí)行reader,writer中對變量v的寫入把x的寫入也刷新到主內(nèi)存中。reader方法中會從主內(nèi)存重新獲取v的值,所以如果線程B看到v的值為true,就能保證拿到的x是42.(因為把x設置成42發(fā)生在把v設置成true之前,volatile禁止這兩個寫入行為的重排序)。
如果變量v不是volatile,那么以上的描述就不成立了,因為執(zhí)行順序可能是v=true, x=42,或者對于線程B來說,根本看不到v被設置成了true。
double-checked locking的問題
臭名昭著的雙重檢查(其中一種單例模式),是一種延遲初始化的實現(xiàn)技巧,避免了同步的開銷,因為在早期的JVM,同步操作性能很差,所以才出現(xiàn)了這樣的小技巧。
private static Something instance = null;public Something getInstance() { if (instance == null) { synchronized (this) { if (instance == null) instance = new Something(); } } return instance;}這個技巧看起來很聰明,避免了同步的開銷,但是有一個問題,它可能不起作用,為什么呢?因為實例的初始化和實例字段的寫入可能被編譯器重排序,這樣就可能返回部門構(gòu)造的對象,結(jié)果就是讀到了一個未初始化完成的對象。
當然,這種bug可以通過使用volatile修飾instance字段進行fix,但是我覺得這種代碼格式實在太丑陋了,如果真要延遲初始化實例,不妨使用下面這種方式:
private static class LazySomethingHolder { public static Something something = new Something();}public static Something getInstance() { return LazySomethingHolder.something;}由于是靜態(tài)字段的初始化,可以確保對訪問該類的所以線程都是可見的。
對于這些,我們需要關心什么
并發(fā)產(chǎn)生的bug非常難以調(diào)試,通常在測試代碼中難以復現(xiàn),當系統(tǒng)負載上來之后,一旦發(fā)生,又很難去捕捉,為了確保程序能夠在任意環(huán)境正確的執(zhí)行,最好是提前花點時間好好思考,雖然很難,但還是比調(diào)試一個線上bug來得容易的多。
總結(jié)
以上是生活随笔為你收集整理的java在线编译器_什么是Java内存模型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux 分区_Linux文件系统、逻
- 下一篇: 一直在构建工作空间_国际资讯Python