真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

Java內(nèi)存模型以及happens-before規(guī)則-創(chuàng)新互聯(lián)

本人免費(fèi)整理了Java高級(jí)資料,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并發(fā)分布式等教程,一共30G,需要自己領(lǐng)取。
傳送門:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q

1. JMM的介紹

在上一篇文章中總結(jié)了雖然這種回答似乎不能獲取什么內(nèi)容,可以google下。在<<深入理解Java虛擬機(jī)>>中看到的定義。原文如下: 當(dāng)多個(gè)線程訪問同一個(gè)對(duì)象時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替運(yùn)行,也不需要進(jìn)行額外的同步,或者在調(diào)用方進(jìn)行任何其他的協(xié)調(diào)操作,調(diào)用這個(gè)對(duì)象的行為都可以獲取正確的結(jié)果,那這個(gè)對(duì)象是線程安全的。

成都創(chuàng)新互聯(lián)公司電話聯(lián)系:18980820575,為您提供成都網(wǎng)站建設(shè)網(wǎng)頁(yè)設(shè)計(jì)及定制高端網(wǎng)站建設(shè)服務(wù),成都創(chuàng)新互聯(lián)公司網(wǎng)頁(yè)制作領(lǐng)域十載,包括石牌坊等多個(gè)行業(yè)擁有豐富的營(yíng)銷推廣經(jīng)驗(yàn),選擇成都創(chuàng)新互聯(lián)公司,為企業(yè)保駕護(hù)航。

關(guān)于定義的理解這是一個(gè)仁者見仁智者見智的事情。出現(xiàn)線程安全的問題一般是因?yàn)橹鲀?nèi)存和工作內(nèi)存數(shù)據(jù)不一致性和重排序?qū)е碌模鉀Q線程安全的問題最重要的就是理解這兩種問題是怎么來(lái)的,那么,理解它們的核心在于理解java內(nèi)存模型(JMM)。

在多線程條件下,多個(gè)線程肯定會(huì)相互協(xié)作完成一件事情,一般來(lái)說就會(huì)涉及到多個(gè)線程間相互通信告知彼此的狀態(tài)以及當(dāng)前的執(zhí)行結(jié)果等,另外,為了性能優(yōu)化,還會(huì)涉及到編譯器指令重排序和處理器指令重排序。下面會(huì)一一來(lái)聊聊這些知識(shí)。


2. 內(nèi)存模型抽象結(jié)構(gòu)

線程間協(xié)作通信可以類比人與人之間的協(xié)作的方式,在現(xiàn)實(shí)生活中,之前網(wǎng)上有個(gè)流行語(yǔ)“你媽喊你回家吃飯了”,就以這個(gè)生活場(chǎng)景為例,小.明在外面玩耍,小.明媽媽在家里做飯,做晚飯后準(zhǔn)備叫小.明回家吃飯,那么就存在兩種方式:

小.明媽媽要去上班了十分緊急這個(gè)時(shí)候手機(jī)又沒有電了,于是就在桌子上貼了一張紙條“飯做好了,放在...”小.明回家后看到紙條如愿吃到媽媽做的飯菜,那么,如果將小.明媽媽和小.明作為兩個(gè)線程,那么這張紙條就是這兩個(gè)線程間通信的共享變量,通過讀寫共享變量實(shí)現(xiàn)兩個(gè)線程間協(xié)作;

還有一種方式就是,媽.媽.的手機(jī)還有電,媽媽在趕去坐公交的路上給小.明打了個(gè)電話,這種方式就是通知機(jī)制來(lái)完成協(xié)作。同樣,可以引申到線程間通信機(jī)制。

通過上面這個(gè)例子,應(yīng)該有些認(rèn)識(shí)。在并發(fā)編程中主要需要解決兩個(gè)問題:1. 線程之間如何通信;2.線程之間如何完成同步(這里的線程指的是并發(fā)執(zhí)行的活動(dòng)實(shí)體)。通信是指線程之間以何種機(jī)制來(lái)交換信息,主要有兩種:共享內(nèi)存和消息傳遞。這里,可以分別類比上面的兩個(gè)舉例。

java內(nèi)存模型是共享內(nèi)存的并發(fā)模型,線程之間主要通過讀-寫共享變量來(lái)完成隱式通信。如果程序員不能理解Java的共享內(nèi)存模型在編寫并發(fā)程序時(shí)一定會(huì)遇到各種各樣關(guān)于內(nèi)存可見性的問題。

1.哪些是共享變量

在java程序中所有實(shí)例域,靜態(tài)域和數(shù)組元素都是放在堆內(nèi)存中(所有線程均可訪問到,是可以共享的),而局部變量,方法定義參數(shù)和異常處理器參數(shù)不會(huì)在線程間共享。共享數(shù)據(jù)會(huì)出現(xiàn)線程安全的問題,而非共享數(shù)據(jù)不會(huì)出現(xiàn)線程安全的問題。關(guān)于JVM運(yùn)行時(shí)內(nèi)存區(qū)域在后面的文章會(huì)講到。

2.JMM抽象結(jié)構(gòu)模型

我們知道CPU的處理速度和主存的讀寫速度不是一個(gè)量級(jí)的,為了平衡這種巨大的差距,每個(gè)CPU都會(huì)有緩存。因此,共享變量會(huì)先放在主存中,每個(gè)線程都有屬于自己的工作內(nèi)存,并且會(huì)把位于主存中的共享變量拷貝到自己的工作內(nèi)存,之后的讀寫操作均使用位于工作內(nèi)存的變量副本,并在某個(gè)時(shí)刻將工作內(nèi)存的變量副本寫回到主存中去。JMM就從抽象層次定義了這種方式,并且JMM決定了一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)其他線程是可見的。

Java內(nèi)存模型以及happens-before規(guī)則

如圖為JMM抽象示意圖,線程A和線程B之間要完成通信的話,要經(jīng)歷如下兩步:

  1. 線程A從主內(nèi)存中將共享變量讀入線程A的工作內(nèi)存后并進(jìn)行操作,之后將數(shù)據(jù)重新寫回到主內(nèi)存中;

  2. 線程B從主存中讀取最新的共享變量

從橫向去看看,線程A和線程B就好像通過共享變量在進(jìn)行隱式通信。這其中有很有意思的問題,如果線程A更新后數(shù)據(jù)并沒有及時(shí)寫回到主存,而此時(shí)線程B讀到的是過期的數(shù)據(jù),這就出現(xiàn)了“臟讀”現(xiàn)象??梢酝ㄟ^同步機(jī)制(控制不同線程間操作發(fā)生的相對(duì)順序)來(lái)解決或者通過volatile關(guān)鍵字使得每次volatile變量都能夠強(qiáng)制刷新到主存,從而對(duì)每個(gè)線程都是可見的。


3. 重排序

一個(gè)好的內(nèi)存模型實(shí)際上會(huì)放松對(duì)處理器和編譯器規(guī)則的束縛,也就是說軟件技術(shù)和硬件技術(shù)都為同一個(gè)目標(biāo)而進(jìn)行奮斗:在不改變程序執(zhí)行結(jié)果的前提下,盡可能提高并行度。JMM對(duì)底層盡量減少約束,使其能夠發(fā)揮自身優(yōu)勢(shì)。因此,在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令進(jìn)行重排序。一般重排序可以分為如下三種:

Java內(nèi)存模型以及happens-before規(guī)則

  1. 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序;

  2. 指令級(jí)并行的重排序?,F(xiàn)代處理器采用了指令級(jí)并行技術(shù)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序;

  3. 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行的。

如圖,1屬于編譯器重排序,而2和3統(tǒng)稱為處理器重排序。這些重排序會(huì)導(dǎo)致線程安全的問題,一個(gè)很經(jīng)典的例子就是DCL問題,這個(gè)在以后的文章中會(huì)具體去聊。針對(duì)編譯器重排序,JMM的編譯器重排序規(guī)則會(huì)禁止一些特定類型的編譯器重排序;針對(duì)處理器重排序,編譯器在生成指令序列的時(shí)候會(huì)通過插入內(nèi)存屏障指令來(lái)禁止某些特殊的處理器重排序。
那么什么情況下,不能進(jìn)行重排序了?下面就來(lái)說說數(shù)據(jù)依賴性。

有如下代碼:

double?pi?=?3.14?//A
?double?r?=?1.0???//B
?double?area?=?pi?*?r?*?r?//C

這是一個(gè)計(jì)算圓面積的代碼,由于A,B之間沒有任何關(guān)系,對(duì)最終結(jié)果也不會(huì)存在關(guān)系,它們之間執(zhí)行順序可以重排序。因此可以執(zhí)行順序可以是A->B->C或者B->A->C執(zhí)行最終結(jié)果都是3.14,即A和B之間沒有數(shù)據(jù)依賴性。

具體的定義為:如果兩個(gè)操作訪問同一個(gè)變量,且這兩個(gè)操作有一個(gè)為寫操作,此時(shí)這兩個(gè)操作就存在數(shù)據(jù)依賴性這里就存在三種情況:1. 讀后寫;2.寫后寫;3. 寫后讀,者三種操作都是存在數(shù)據(jù)依賴性的,如果重排序會(huì)對(duì)最終執(zhí)行結(jié)果會(huì)存在影響。

編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴性關(guān)系的兩個(gè)操作的執(zhí)行順序

另外,還有一個(gè)比較有意思的就是as-if-serial語(yǔ)義。

as-if-serial

as-if-serial語(yǔ)義的意思是:不管怎么重排序(編譯器和處理器為了提供并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器,runtime和處理器都必須遵守as-if-serial語(yǔ)義。as-if-serial語(yǔ)義把單線程程序保護(hù)了起來(lái),遵守as-if-serial語(yǔ)義的編譯器,runtime和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個(gè)幻覺:?jiǎn)尉€程程序是按程序的順序來(lái)執(zhí)行的。

比如上面計(jì)算圓面積的代碼,在單線程中,會(huì)讓人感覺代碼是一行一行順序執(zhí)行上,實(shí)際上A,B兩行不存在數(shù)據(jù)依賴性可能會(huì)進(jìn)行重排序,即A,B不是順序執(zhí)行的。as-if-serial語(yǔ)義使程序員不必?fù)?dān)心單線程中重排序的問題干擾他們,也無(wú)需擔(dān)心內(nèi)存可見性問題。

4. happens-before規(guī)則

上面的內(nèi)容講述了重排序原則,一會(huì)是編譯器重排序一會(huì)是處理器重排序,如果讓程序員再去了解這些底層的實(shí)現(xiàn)以及具體規(guī)則,那么程序員的負(fù)擔(dān)就太重了,嚴(yán)重影響了并發(fā)編程的效率。因此,JMM為程序員在上層提供了六條規(guī)則,這樣我們就可以根據(jù)規(guī)則去推論跨線程的內(nèi)存可見性問題,而不用再去理解底層重排序的規(guī)則。下面以兩個(gè)方面來(lái)說。

4.1 happens-before定義
happens-before的概念最初由Leslie Lamport在其一篇影響深遠(yuǎn)的論文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出,有興趣的可以google一下。JSR-133使用happens-before的概念來(lái)指定兩個(gè)操作之間的執(zhí)行順序。由于這兩個(gè)操作可以在一個(gè)線程之內(nèi),也可以是在不同線程之間。

因此,JMM可以通過happens-before關(guān)系向程序員提供跨線程的內(nèi)存可見性保證(如果A線程的寫操作a與B線程的讀操作b之間存在happens-before關(guān)系,盡管a操作和b操作在不同的線程中執(zhí)行,但JMM向程序員保證a操作將對(duì)b操作可見)。

具體的定義為:
1)如果一個(gè)操作happens-before另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見,而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。

2)兩個(gè)操作之間存在happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照happens-before關(guān)系指定的順序來(lái)執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來(lái)執(zhí)行的結(jié)果一致,那么這種重排序并不非法(也就是說,JMM允許這種重排序)。

上面的1)是JMM對(duì)程序員的承諾。從程序員的角度來(lái)說,可以這樣理解happens-before關(guān)系:如果A happens-before B,那么Java內(nèi)存模型將向程序員保證——A操作的結(jié)果將對(duì)B可見,且A的執(zhí)行順序排在B之前。注意,這只是Java內(nèi)存模型向程序員做出的保證!

上面的2)是JMM對(duì)編譯器和處理器重排序的約束原則。正如前面所言,JMM其實(shí)是在遵循一個(gè)基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。

JMM這么做的原因是:程序員對(duì)于這兩個(gè)操作是否真的被重排序并不關(guān)心,程序員關(guān)心的是程序執(zhí)行時(shí)的語(yǔ)義不能被改變(即執(zhí)行結(jié)果不能被改變)。因此,happens-before關(guān)系本質(zhì)上和as-if-serial語(yǔ)義是一回事。

下面來(lái)比較一下as-if-serial和happens-before:

as-if-serial VS happens-before

  1. as-if-serial語(yǔ)義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。

  2. as-if-serial語(yǔ)義給編寫單線程程序的程序員創(chuàng)造了一個(gè)幻境:?jiǎn)尉€程程序是按程序的順序來(lái)執(zhí)行的。happens-before關(guān)系給編寫正確同步的多線程程序的程序員創(chuàng)造了一個(gè)幻境:正確同步的多線程程序是按happens-before指定的順序來(lái)執(zhí)行的。

  3. as-if-serial語(yǔ)義和happens-before這么做的目的,都是為了在不改變程序執(zhí)行結(jié)果的前提下,盡可能地提高程序執(zhí)行的并行度。

4.2 具體規(guī)則
具體的一共有六項(xiàng)規(guī)則:

  1. 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作。

  2. 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖。

  3. volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀。

  4. 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。

  5. start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動(dòng)線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。

  6. join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。

  7. 程序中斷規(guī)則:對(duì)線程interrupted()方法的調(diào)用先行于被中斷線程的代碼檢測(cè)到中斷時(shí)間的發(fā)生。

  8. 對(duì)象finalize規(guī)則:一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行于發(fā)生它的finalize()方法的開始。

下面以一個(gè)具體的例子來(lái)講下如何使用這些規(guī)則進(jìn)行推論:

依舊以上面計(jì)算圓面積的進(jìn)行描述。利用程序順序規(guī)則(規(guī)則1)存在三個(gè)happens-before關(guān)系:
1. A happens-before B;
2. B happens-before C;
3. A happens-before C。
這里的第三個(gè)關(guān)系是利用傳遞性進(jìn)行推論的。
A happens-before B,定義1要求A執(zhí)行結(jié)果對(duì)B可見,并且A操作的執(zhí)行順序在B操作之前,但與此同時(shí)利用定義中的第二條,A,B操作彼此不存在數(shù)據(jù)依賴性,兩個(gè)操作的執(zhí)行順序?qū)ψ罱K結(jié)果都不會(huì)產(chǎn)生影響,在不改變最終結(jié)果的前提下,允許A,B兩個(gè)操作重排序,即happens-before關(guān)系并不代表了最終的執(zhí)行順序。

5. 總結(jié)
上面已經(jīng)聊了關(guān)于JMM的兩個(gè)方面:
1. JMM的抽象結(jié)構(gòu)(主內(nèi)存和線程工作內(nèi)存);
2. 重排序以及happens-before規(guī)則。

接下來(lái),我們來(lái)做一個(gè)總結(jié)。從兩個(gè)方面進(jìn)行考慮。
1. 如果讓我們?cè)O(shè)計(jì)JMM應(yīng)該從哪些方面考慮,也就是說JMM承擔(dān)哪些功能;
2. happens-before與JMM的關(guān)系;
3. 由于JMM,多線程情況下可能會(huì)出現(xiàn)哪些問題?
5.1 JMM的設(shè)計(jì)

Java內(nèi)存模型以及happens-before規(guī)則

JMM是語(yǔ)言級(jí)的內(nèi)存模型,在我的理解中JMM處于中間層,包含了兩個(gè)方面:
(1)內(nèi)存模型;
(2)重排序以及happens-before規(guī)則。
同時(shí),為了禁止特定類型的重排序會(huì)對(duì)編譯器和處理器指令序列加以控制。
而上層會(huì)有基于JMM的關(guān)鍵字和J.U.C包下的一些具體類用來(lái)方便程序員能夠迅速高效率的進(jìn)行并發(fā)編程。站在JMM設(shè)計(jì)者的角度,在設(shè)計(jì)JMM時(shí)需要考慮兩個(gè)關(guān)鍵因素:

  1. 程序員對(duì)內(nèi)存模型的使用?程序員希望內(nèi)存模型易于理解、易于編程。程序員希望基于一個(gè)強(qiáng)內(nèi)存模型來(lái)編寫代碼。

  2. 編譯器和處理器對(duì)內(nèi)存模型的實(shí)現(xiàn)?編譯器和處理器希望內(nèi)存模型對(duì)它們的束縛越少越好,這樣它們就可以做盡可能多的優(yōu)化來(lái)提高性能。編譯器和處理器希望實(shí)現(xiàn)一個(gè)弱內(nèi)存模型。

另外還要一個(gè)特別有意思的事情就是關(guān)于重排序問題,更簡(jiǎn)單的說,重排序可以分為兩類:

  1. 會(huì)改變程序執(zhí)行結(jié)果的重排序。

  2. 不會(huì)改變程序執(zhí)行結(jié)果的重排序。

JMM對(duì)這兩種不同性質(zhì)的重排序,采取了不同的策略,如下。

  1. 對(duì)于會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。

  2. 對(duì)于不會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM對(duì)編譯器和處理器不做要求(JMM允許這種 重排序)

JMM的設(shè)計(jì)圖為:

Java內(nèi)存模型以及happens-before規(guī)則

從圖可以看出:

  1. JMM向程序員提供的happens-before規(guī)則能滿足程序員的需求。JMM的happens-before規(guī)則不但簡(jiǎn)單易懂,而且也向程序員提供了足夠強(qiáng)的內(nèi)存可見性保證(有些內(nèi)存可見性保證其實(shí)并不一定真實(shí)存在,比如上面的A happens-before B)。

  2. JMM對(duì)編譯器和處理器的束縛已經(jīng)盡可能少。從上面的分析可以看出,JMM其實(shí)是在遵循一個(gè)基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。例如,如果編譯器經(jīng)過細(xì)致的分析后,認(rèn)定一個(gè)鎖只會(huì)被單個(gè)線程訪問,那么這個(gè)鎖可以被消除。再如,如果編譯器經(jīng)過細(xì)致的分析后,認(rèn)定一個(gè)volatile變量只會(huì)被單個(gè)線程訪問,那么編譯器可以把這個(gè)volatile變量當(dāng)作一個(gè)普通變量來(lái)對(duì)待。這些優(yōu)化既不會(huì)改變程序的執(zhí)行結(jié)果,又能提高程序的執(zhí)行效率。

5.2 happens-before與JMM的關(guān)系

Java內(nèi)存模型以及happens-before規(guī)則

一個(gè)happens-before規(guī)則對(duì)應(yīng)于一個(gè)或多個(gè)編譯器和處理器重排序規(guī)則。對(duì)于Java程序員來(lái)說,happens-before規(guī)則簡(jiǎn)單易懂,它避免Java程序員為了理解JMM提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實(shí)現(xiàn)方法

5.3 今后可能需要關(guān)注的問題
從上面內(nèi)存抽象結(jié)構(gòu)來(lái)說,可能出在數(shù)據(jù)“臟讀”的現(xiàn)象,這就是數(shù)據(jù)可見性的問題,另外,重排序在多線程中不注意的話也容易存在一些問題,比如一個(gè)很經(jīng)典的問題就是DCL(雙重檢驗(yàn)鎖),這就是需要禁止重排序,另外,在多線程下原子操作例如i++不加以注意的也容易出現(xiàn)線程安全的問題。但總的來(lái)說,在多線程開發(fā)時(shí)需要從原子性,有序性,可見性三個(gè)方面進(jìn)行考慮。J.U.C包下的并發(fā)工具類和并發(fā)容器也是需要花時(shí)間去掌握的,這些東西在以后得文章中多會(huì)一一進(jìn)行討論。

創(chuàng)新互聯(lián)www.cdcxhl.cn,專業(yè)提供香港、美國(guó)云服務(wù)器,動(dòng)態(tài)BGP最優(yōu)骨干路由自動(dòng)選擇,持續(xù)穩(wěn)定高效的網(wǎng)絡(luò)助力業(yè)務(wù)部署。公司持有工信部辦法的idc、isp許可證, 機(jī)房獨(dú)有T級(jí)流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確進(jìn)行流量調(diào)度,確保服務(wù)器高可用性。佳節(jié)活動(dòng)現(xiàn)已開啟,新人活動(dòng)云服務(wù)器買多久送多久。


網(wǎng)站標(biāo)題:Java內(nèi)存模型以及happens-before規(guī)則-創(chuàng)新互聯(lián)
網(wǎng)站地址:http://weahome.cn/article/epcsi.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部