国产成人精品亚洲777人妖,欧美日韩精品一区视频,最新亚洲国产,国产乱码精品一区二区亚洲

您的位置:首頁技術(shù)文章
文章詳情頁

Java內(nèi)存模型(JMM)及happens-before原理

瀏覽:130日期:2022-09-02 13:22:02

我們知道java程序是運(yùn)行在JVM中的,而JVM就是構(gòu)建在內(nèi)存上的虛擬機(jī),那么內(nèi)存模型JMM是做什么用的呢?

我們考慮一個(gè)簡(jiǎn)單的賦值問題:

int a=100;

JMM考慮的就是什么情況下讀取變量a的線程可以看到值為100??雌饋磉@是一個(gè)很簡(jiǎn)單的問題,賦值之后不就可以讀到值了嗎?

但是上面的只是我們?cè)创a的編寫順序,當(dāng)把源碼編譯之后,在編譯器中生成的指令的順序跟源碼的順序并不是完全一致的。處理器可能采用亂序或者并行的方式來執(zhí)行指令(在JVM中只要程序的最終執(zhí)行結(jié)果和在嚴(yán)格串行環(huán)境中執(zhí)行結(jié)果一致,這種重排序是允許的)。并且處理器還有本地緩存,當(dāng)將結(jié)果存儲(chǔ)在本地緩存中,其他線程是無法看到結(jié)果的。除此之外緩存提交到主內(nèi)存的順序也肯能會(huì)變化。

上面提到的種種可能都會(huì)導(dǎo)致在多線程環(huán)境中產(chǎn)生不同的結(jié)果。在多線程環(huán)境中,大部分時(shí)間多線程都是在執(zhí)行各自的任務(wù),只有在多個(gè)線程需要共享數(shù)據(jù)的時(shí)候,才需要協(xié)調(diào)線程之間的操作。

而JMM就是JVM中必須遵守的一組最小保證,它規(guī)定了對(duì)于變量的寫入操作在什么時(shí)候?qū)ζ渌€程是可見的。

重排序

上面講了JVM中的重排序,這里我們舉個(gè)例子,以便大家對(duì)重排序有一個(gè)更深入的理解:

@Slf4jpublic class Reorder { int x=0, y=0; int a=0, b=0; private void reorderMethod() throws InterruptedException { Thread one = new Thread(()->{ a=1; x=b; }); Thread two = new Thread(()->{ b=1; y=a; }); one.start(); two.start(); one.join(); two.join(); log.info('{},{}', x, y); } public static void main(String[] args) throws InterruptedException { for (int i=0; i< 100; i++){ new Reorder().reorderMethod(); } }}

上面的例子是一個(gè)很簡(jiǎn)單的并發(fā)程序。由于我們沒有使用同步限制,所以線程one和two的執(zhí)行順序是不定的。有可能one在two之前執(zhí)行,也有可能在two之后執(zhí)行,也可能兩者同時(shí)執(zhí)行。不同的執(zhí)行順序可能會(huì)導(dǎo)致不同的輸出結(jié)果。

同時(shí)雖然我們?cè)诖a中指定了先執(zhí)行a=1, 再執(zhí)行x=b,但是這兩條語句實(shí)際上是沒有關(guān)系的,在JVM中完全可能將兩條語句重排序成x=b在前,a=1在后,從而導(dǎo)致輸出更多意想不到的結(jié)果。

Happens-Before

為了保證java內(nèi)存模型中的操作順序,JMM為程序中的所有操作定義了一個(gè)順序關(guān)系,這個(gè)順序叫做Happens-Before。要想保證操作B看到操作A的結(jié)果,不管A和B是在同一線程還是不同線程,那么A和B必須滿足Happens-Before的關(guān)系。如果兩個(gè)操作不滿足happens-before的關(guān)系,那么JVM可以對(duì)他們?nèi)我庵嘏判颉?/p>

我們看一下happens-before的規(guī)則:

1.程序順序規(guī)則: 如果在程序中操作A在操作B之前,那么在同一個(gè)線程中操作A將會(huì)在操作B之前執(zhí)行。

注意,這里的操作A在操作B之前執(zhí)行是指在單線程環(huán)境中,雖然虛擬機(jī)會(huì)對(duì)相應(yīng)的指令進(jìn)行重排序,但是最終的執(zhí)行結(jié)果跟按照代碼順序執(zhí)行是一樣的。虛擬機(jī)只會(huì)對(duì)不存在依賴的代碼進(jìn)行重排序。

2.監(jiān)視器鎖規(guī)則: 監(jiān)視器上的解鎖操作必須在同一個(gè)監(jiān)視器上面的加鎖操作之前執(zhí)行。鎖我們大家都很清楚了,這里的順序必須指的是同一個(gè)鎖,如果是在不同的鎖上面,那么其執(zhí)行順序也不能得到保證。

3.volatile變量規(guī)則: 對(duì)volatile變量的寫入操作必須在對(duì)該變量的讀操作之前執(zhí)行。原子變量和volatile變量在讀寫操作上面有著相同的語義。

4.線程啟動(dòng)規(guī)則: 線程上對(duì)Thread.start的操作必須要在該線程中執(zhí)行任何操作之前執(zhí)行。

5.線程結(jié)束規(guī)則: 線程中的任何操作都必須在其他線程檢測(cè)到該線程結(jié)束之前執(zhí)行。

6.中斷規(guī)則: 當(dāng)一個(gè)線程再另一個(gè)線程上調(diào)用interrupt時(shí),必須在被中斷線程檢測(cè)到interrupt調(diào)用之前執(zhí)行。

7.終結(jié)器規(guī)則: 對(duì)象的構(gòu)造函數(shù)必須在啟動(dòng)該對(duì)象的終結(jié)器之前執(zhí)行完畢。

8.傳遞性: 如果操作A在操作B之前執(zhí)行,并且操作B在操作C之前執(zhí)行,那么操作A必須在操作C之前執(zhí)行。

上面的規(guī)則2很好理解,在加鎖的過程中,不允許其他的線程獲得該鎖,也意味著其他的線程必須等待鎖釋放之后才能加鎖和執(zhí)行其業(yè)務(wù)邏輯。

4,5,6,7規(guī)則也很好理解,只有開始,才能結(jié)束。這符合我們對(duì)程序的一般認(rèn)識(shí)。

8的傳遞性相信學(xué)過數(shù)學(xué)的人應(yīng)該也不難理解。

接下來我們重點(diǎn)討論一下規(guī)則3和規(guī)則1的結(jié)合。討論之前我們?cè)倏偨Y(jié)一下happens-before到底是做什么的。

因?yàn)镴VM會(huì)對(duì)接收到的指令進(jìn)行重排序,為了保證指令的執(zhí)行順序,我們才有了happens-before規(guī)則。上面講到的2,3,4,5,6,7規(guī)則可以看做是重排序的節(jié)點(diǎn),這些節(jié)點(diǎn)是不允許重排序的,只有在這些節(jié)點(diǎn)之間的指令才允許重排序。

結(jié)合規(guī)則1程序順序規(guī)則,我們得到其真正的含義:代碼中寫在重排序節(jié)點(diǎn)之前的指令,一定會(huì)在重排序節(jié)點(diǎn)執(zhí)行之前執(zhí)行。

重排序節(jié)點(diǎn)就是一個(gè)分界點(diǎn),它的位置是不能夠移動(dòng)的。看一下下面的直觀例子:

Java內(nèi)存模型(JMM)及happens-before原理

線程1中有兩個(gè)指令:set i=1, set volatile a=2。

線程2中也有兩個(gè)指令:get volatile a, get i。

按照上面的理論,set和get volatile是兩個(gè)重排序節(jié)點(diǎn),set必須排在get之前。而依據(jù)規(guī)則1,代碼中set i=1 在set volatile a=2之前,因?yàn)閟et volatile是重排序節(jié)點(diǎn),所以需要遵守程序順序執(zhí)行規(guī)則,從而set i=1要在set volatile a=2之前執(zhí)行。同樣的道理get volatile a在get i之前執(zhí)行。最后導(dǎo)致i=1在get i之前執(zhí)行。

這個(gè)操作叫做借助同步。

安全發(fā)布

我們經(jīng)常會(huì)用到單例模式來創(chuàng)建一個(gè)單的對(duì)象,我們看下下面的方法有什么不妥:

public class Book { private static Book book; public static Book getBook(){ if(book==null){ book = new Book(); } return book; }}

上面的類中定義了一個(gè)getBook方法來返回一個(gè)新的book對(duì)象,返回對(duì)象之前,我們先判斷了book是否為空,如果不為空的話就new一個(gè)book對(duì)象。

初看起來,好像沒什么問題,但是如果仔細(xì)考慮JMM的重排規(guī)則,就會(huì)發(fā)現(xiàn)問題所在。book=new Book()其實(shí)一個(gè)復(fù)雜的命令,并不是原子性操作。它大概可以分解為1.分配內(nèi)存,2.實(shí)例化對(duì)象,3.將對(duì)象和內(nèi)存地址建立關(guān)聯(lián)。

其中2和3有可能會(huì)被重排序,然后就有可能出現(xiàn)book返回了,但是還沒有初始化完畢的情況。從而出現(xiàn)不可以預(yù)見的錯(cuò)誤。

根據(jù)上面我們講到的happens-before規(guī)則, 最簡(jiǎn)單的辦法就是給方法前面加上synchronized關(guān)鍵字:

public class Book { private static Book book; public synchronized static Book getBook(){ if(book==null){ book = new Book(); } return book; }}

我們?cè)倏聪旅嬉环N靜態(tài)域的實(shí)現(xiàn):

public class BookStatic { private static BookStatic bookStatic= new BookStatic(); public static BookStatic getBookStatic(){ return bookStatic; }}

JVM在類被加載之后和被線程使用之前,會(huì)進(jìn)行靜態(tài)初始化,而在這個(gè)初始化階段將會(huì)獲得一個(gè)鎖,從而保證在靜態(tài)初始化階段內(nèi)存寫入操作將對(duì)所有的線程可見。

上面的例子定義了static變量,在靜態(tài)初始化階段將會(huì)被實(shí)例化。這種方式叫做提前初始化。

下面我們?cè)倏匆粋€(gè)延遲初始化占位類的模式:

public class BookStaticLazy { private static class BookStaticHolder{ private static BookStaticLazy bookStatic= new BookStaticLazy(); } public static BookStaticLazy getBookStatic(){ return BookStaticHolder.bookStatic; }}

上面的類中,只有在調(diào)用getBookStatic方法的時(shí)候才會(huì)去初始化類。

接下來我們?cè)俳榻B一下雙重檢查加鎖。

public class BookDLC { private volatile static BookDLC bookDLC; public static BookDLC getBookDLC(){ if(bookDLC == null ){ synchronized (BookDLC.class){if(bookDLC ==null){ bookDLC=new BookDLC();} } } return bookDLC; }}

上面的類中檢測(cè)了兩次bookDLC的值,只有bookDLC為空的時(shí)候才進(jìn)行加鎖操作。看起來一切都很完美,但是我們要注意一點(diǎn),這里bookDLC一定要是volatile。

因?yàn)閎ookDLC的賦值操作和返回操作并沒有happens-before,所以可能會(huì)出現(xiàn)獲取到一個(gè)僅部分構(gòu)造的實(shí)例。這也是為什么我們要加上volatile關(guān)鍵詞。

初始化安全性

本文的最后,我們將討論一下在構(gòu)造函數(shù)中含有final域的對(duì)象初始化。

對(duì)于正確構(gòu)造的對(duì)象,初始化對(duì)象保證了所有的線程都能夠正確的看到由構(gòu)造函數(shù)為對(duì)象給各個(gè)final域設(shè)置的正確值,包括final域可以到達(dá)的任何變量(比如final數(shù)組中的元素,final的hashMap等)。

public class FinalSafe { private final HashMap<String,String> hashMap; public FinalSafe(){ hashMap= new HashMap<>(); hashMap.put('key1','value1'); }}

上面的例子中,我們定義了一個(gè)final對(duì)象,并且在構(gòu)造函數(shù)中初始化了這個(gè)對(duì)象。那么這個(gè)final對(duì)象是將不會(huì)跟構(gòu)造函數(shù)之后的其他操作重排序。

本文的例子可以參考https://github.com/ddean2009/learn-java-concurrency/tree/master/reorder

以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持好吧啦網(wǎng)。

標(biāo)簽: Java
相關(guān)文章:
主站蜘蛛池模板: 锡林浩特市| 吴江市| 交城县| 和林格尔县| 浠水县| 镇康县| 容城县| 台东市| 仙居县| 宜州市| 凯里市| 万源市| 阜宁县| 巴楚县| 甘洛县| 漳州市| 从江县| 平湖市| 天长市| 银川市| 云南省| 漳平市| 黔江区| 张家川| 平江县| 东兰县| 虞城县| 游戏| 施秉县| 峨边| 巫溪县| 六枝特区| 贺州市| 丰都县| 威远县| 扎囊县| 兴业县| 沁阳市| 喀什市| 城口县| 普宁市|