无招胜有招之Java进阶JVM(四)内存模型plus
一、計(jì)算機(jī)內(nèi)存模型:
在多CPU的系統(tǒng)中,每個(gè)CPU都有多級(jí)緩存,一般分為L(zhǎng)1、L2、L3緩存,因?yàn)檫@些緩存的存在,提供了數(shù)據(jù)的訪問(wèn)性能,也減輕了數(shù)據(jù)總線上數(shù)據(jù)傳輸?shù)膲毫?#xff0c;同時(shí)也帶來(lái)了很多新的挑戰(zhàn),比如兩個(gè)CPU同時(shí)去操作同一個(gè)內(nèi)存地址,會(huì)發(fā)生什么?在什么條件下,它們可以看到相同的結(jié)果?這些都是需要解決的。
所以在CPU的層面,內(nèi)存模型定義了一個(gè)充分必要條件,保證其它CPU的寫入動(dòng)作對(duì)該CPU是可見(jiàn)的,而且該CPU的寫入動(dòng)作對(duì)其它CPU也是可見(jiàn)的,那這種可見(jiàn)性,應(yīng)該如何實(shí)現(xiàn)呢?
有些處理器提供了強(qiáng)內(nèi)存模型,所有CPU在任何時(shí)候都能看到內(nèi)存中任意位置相同的值,這種完全是硬件提供的支持。
其它處理器,提供了弱內(nèi)存模型,需要執(zhí)行一些特殊指令(就是經(jīng)常看到或者聽到的,memory barriers內(nèi)存屏障),刷新CPU緩存的數(shù)據(jù)到內(nèi)存中,保證這個(gè)寫操作能夠被其它CPU可見(jiàn),或者將CPU緩存的數(shù)據(jù)設(shè)置為無(wú)效狀態(tài),保證其它CPU的寫操作對(duì)本CPU可見(jiàn)。通常這些內(nèi)存屏障的行為由底層實(shí)現(xiàn),對(duì)于上層語(yǔ)言的程序員來(lái)說(shuō)是透明的(不需要太關(guān)心具體的內(nèi)存屏障如何實(shí)現(xiàn))。
前面說(shuō)到的內(nèi)存屏障,除了實(shí)現(xiàn)CPU之前的數(shù)據(jù)可見(jiàn)性之外,還有一個(gè)重要的職責(zé),可以禁止指令的重排序。
這里說(shuō)的重排序可以發(fā)生在好幾個(gè)地方:編譯器、運(yùn)行時(shí)、JIT等,比如編譯器會(huì)覺(jué)得把一個(gè)變量的寫操作放在最后會(huì)更有效率,編譯后,這個(gè)指令就在最后了(前提是只要不改變程序的語(yǔ)義,編譯器、執(zhí)行器就可以這樣自由的隨意優(yōu)化),一旦編譯器對(duì)某個(gè)變量的寫操作進(jìn)行優(yōu)化(放到最后),那么在執(zhí)行之前,另一個(gè)線程將不會(huì)看到這個(gè)執(zhí)行結(jié)果。
當(dāng)然了,寫入動(dòng)作可能被移到后面,那也有可能被挪到了前面,這樣的“優(yōu)化”有什么影響呢?這種情況下,其它線程可能會(huì)在程序?qū)崿F(xiàn)“發(fā)生”之前,看到這個(gè)寫入動(dòng)作(這里怎么理解,指令已經(jīng)執(zhí)行了,但是在代碼層面還沒(méi)執(zhí)行到)。通過(guò)內(nèi)存屏障的功能,我們可以禁止一些不必要、或者會(huì)帶來(lái)負(fù)面影響的重排序優(yōu)化,在內(nèi)存模型的范圍內(nèi),實(shí)現(xiàn)更高的性能,同時(shí)保證程序的正確性。
二、緩存一致性:
百度百科:指保留在高速緩存中的共享資源,保持?jǐn)?shù)據(jù)一致性的機(jī)制。
CPU的高速緩存當(dāng)中在單線程運(yùn)行是沒(méi)有問(wèn)題的,但是在多線程中運(yùn)行就會(huì)有問(wèn)題了。在多核CPU中,每條線程可能運(yùn)行于不同的CPU中,因此每個(gè)線程運(yùn)行時(shí)有自己的高速緩存(對(duì)單核CPU來(lái)說(shuō),其實(shí)也會(huì)出現(xiàn)這種問(wèn)題,只不過(guò)是以線程調(diào)度的形式來(lái)分別執(zhí)行的)。這時(shí)CPU緩存中的值可能和緩存中的值不一樣,這就是著名的緩存一致性問(wèn)題
緩存一致性可以分為三個(gè)層級(jí):
(1)在進(jìn)行每個(gè)寫入運(yùn)算時(shí)都立刻采取措施保證數(shù)據(jù)一致性
(2)每個(gè)獨(dú)立的運(yùn)算,假如它造成數(shù)據(jù)值的改變,所有進(jìn)程都可以看到一致的改變結(jié)果
(3)在每次運(yùn)算之后,不同的進(jìn)程可能會(huì)看到不同的值(這也就是沒(méi)有一致性的行為)
為了解決緩存不一致性問(wèn)題,通常來(lái)說(shuō)有以下2種解決方法:
(1)通過(guò)在總線加LOCK#鎖的方式
(2)通過(guò)緩存一致性協(xié)議
三、MESI協(xié)議
百度百科指MESI協(xié)議是基于Invalidate的高速緩存一致性協(xié)議,并且是支持回寫高速緩存的最常用協(xié)議之一。
在該協(xié)議的作用下,雖然各cache控制器隨時(shí)都在監(jiān)聽系統(tǒng)總線,但能監(jiān)聽到的只有讀未命中、寫未命中以及共享行寫命中三種情況。讀監(jiān)聽命中的有效行都要進(jìn)入S態(tài)并發(fā)出監(jiān)聽命中指示,但M態(tài)行要搶先寫回主存;寫監(jiān)聽命中的有效行都要進(jìn)入I態(tài),但收到RWITM時(shí)的M態(tài)行要搶先寫回主存。總之監(jiān)控邏輯并不復(fù)雜,增添的系統(tǒng)總線傳輸開銷也不大,但MESI協(xié)議卻有力地保證了主存塊臟拷貝在多cache中的一致性,并能及時(shí)寫回,保證cache主存存取的正確性。
- 可見(jiàn)性
Java內(nèi)存模型是通過(guò)在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值的這種依賴主內(nèi)存作為傳遞媒介的方式來(lái)實(shí)現(xiàn)的。
Java中的volatile關(guān)鍵字提供了一個(gè)功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次是用之前都從主內(nèi)存刷新。因此,可以使用volatile來(lái)保證多線程操作時(shí)變量的可見(jiàn)性。
除了volatile,Java中的synchronized和final兩個(gè)關(guān)鍵字也可以實(shí)現(xiàn)可見(jiàn)性。只不過(guò)實(shí)現(xiàn)方式不同,這里不再展開了。
- 原子性
在Java中,為了保證原子性,提供了兩個(gè)高級(jí)的字節(jié)碼指令monitorenter和monitorexit。在synchronized的實(shí)現(xiàn)原理文章中,介紹過(guò),這兩個(gè)字節(jié)碼,在Java中對(duì)應(yīng)的關(guān)鍵字就是synchronized。
因此,在Java中可以使用synchronized來(lái)保證方法和代碼塊內(nèi)的操作是原子性的。
- 順序性
在Java中,可以使用synchronized和volatile來(lái)保證多線程之間操作的有序性。實(shí)現(xiàn)方式有所區(qū)別:
volatile關(guān)鍵字會(huì)禁止指令重排。synchronized關(guān)鍵字保證同一時(shí)刻只允許一條線程操作。
- happens-before
因?yàn)閖vm會(huì)對(duì)代碼進(jìn)行編譯優(yōu)化,指令會(huì)出現(xiàn)重排序的情況,為了避免編譯優(yōu)化對(duì)并發(fā)編程安全性的影響,需要happens-before規(guī)則定義一些禁止編譯優(yōu)化的場(chǎng)景,保證并發(fā)編程的正確性?。
1. 規(guī)則一:程序的順序性規(guī)則
一個(gè)線程中,按照程序的順序,前面的操作happens-before后續(xù)的任何操作。
對(duì)于這一點(diǎn),可能會(huì)有疑問(wèn)。順序性是指,我們可以按照順序推演程序的執(zhí)行結(jié)果,但是編譯器未必一定會(huì)按照這個(gè)順序編譯,但是編譯器保證結(jié)果一定==順序推演的結(jié)果。
2. 規(guī)則二:volatile規(guī)則
對(duì)一個(gè)volatile變量的寫操作,happens-before后續(xù)對(duì)這個(gè)變量的讀操作。
3. 規(guī)則三:傳遞性規(guī)則
如果A happens-before B,B happens-before C,那么A happens-before C。
jdk1.5的增強(qiáng)就體現(xiàn)在這里。回到上面例子中,線程A中,根據(jù)規(guī)則一,對(duì)變量x的寫操作是happens-before對(duì)變量v的寫操作的,根據(jù)規(guī)則二,對(duì)變量v的寫操作是happens-before對(duì)變量v的讀操作的,最后根據(jù)規(guī)則三,也就是說(shuō),線程A對(duì)變量x的寫操作,一定happens-before線程B對(duì)v的讀操作,那么線程B在注釋處讀到的變量x的值,一定是42.
4.規(guī)則四:管程中的鎖規(guī)則
對(duì)一個(gè)鎖的解鎖操作,happens-before后續(xù)對(duì)這個(gè)鎖的加鎖操作。
這一點(diǎn)不難理解。
5.規(guī)則五:線程start()規(guī)則
主線程A啟動(dòng)線程B,線程B中可以看到主線程啟動(dòng)B之前的操作。也就是start() happens before 線程B中的操作。
6.規(guī)則六:線程join()規(guī)則
主線程A等待子線程B完成,當(dāng)子線程B執(zhí)行完畢后,主線程A可以看到線程B的所有操作。也就是說(shuō),子線程B中的任意操作,happens-before join()的返回。
- 內(nèi)存屏障
內(nèi)存屏障,也稱內(nèi)存柵欄,內(nèi)存柵障,屏障指令等, 是一類同步屏障指令,是CPU或編譯器在對(duì)內(nèi)存隨機(jī)訪問(wèn)的操作中的一個(gè)同步點(diǎn),使得此點(diǎn)之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點(diǎn)之后的操作。
- Synchronized
Synchronization有多種語(yǔ)義,其中最容易理解的是互斥,對(duì)于一個(gè)monitor對(duì)象,只能夠被一個(gè)線程持有,意味著一旦有線程進(jìn)入了同步代碼塊,那么其它線程就不能進(jìn)入直到第一個(gè)進(jìn)入的線程退出代碼塊(這因?yàn)槎寄芾斫?#xff09;。
但是更多的時(shí)候,使用synchronization并非單單互斥功能,Synchronization保證了線程在同步塊之前或者期間寫入動(dòng)作,對(duì)于后續(xù)進(jìn)入該代碼塊的線程是可見(jiàn)的(又是可見(jiàn)性,不過(guò)這里需要注意是對(duì)同一個(gè)monitor對(duì)象而言)。在一個(gè)線程退出同步塊時(shí),線程釋放monitor對(duì)象,它的作用是把CPU緩存數(shù)據(jù)(本地緩存數(shù)據(jù))刷新到主內(nèi)存中,從而實(shí)現(xiàn)該線程的行為可以被其它線程看到。在其它線程進(jìn)入到該代碼塊時(shí),需要獲得monitor對(duì)象,它在作用是使CPU緩存失效,從而使變量從主內(nèi)存中重新加載,然后就可以看到之前線程對(duì)該變量的修改。
但從緩存的角度看,似乎這個(gè)問(wèn)題只會(huì)影響多處理器的機(jī)器,對(duì)于單核來(lái)說(shuō)沒(méi)什么問(wèn)題,但是別忘了,它還有一個(gè)語(yǔ)義是禁止指令的重排序,對(duì)于編譯器來(lái)說(shuō),同步塊中的代碼不會(huì)移動(dòng)到獲取和釋放monitor外面。
- Volatile
Volatile字段主要用于線程之間進(jìn)行通信,volatile字段的每次讀行為都能看到其它線程最后一次對(duì)該字段的寫行為,通過(guò)它就可以避免拿到緩存中陳舊數(shù)據(jù)。它們必須保證在被寫入之后,會(huì)被刷新到主內(nèi)存中,這樣就可以立即對(duì)其它線程可以見(jiàn)。類似的,在讀取volatile字段之前,緩存必須是無(wú)效的,以保證每次拿到的都是主內(nèi)存的值,都是最新的值。volatile的內(nèi)存語(yǔ)義和sychronize獲取和釋放monitor的實(shí)現(xiàn)目的是差不多的。
對(duì)于重新排序,volatile也有額外的限制
- Final
如果一個(gè)類包含final字段,且在構(gòu)造函數(shù)中初始化,那么正確的構(gòu)造一個(gè)對(duì)象后,final字段被設(shè)置后對(duì)于其它線程是可見(jiàn)的。
這里所說(shuō)的正確構(gòu)造對(duì)象,意思是在對(duì)象的構(gòu)造過(guò)程中,不允許對(duì)該對(duì)象進(jìn)行引用,不然的話,可能存在其它線程在對(duì)象還沒(méi)構(gòu)造完成時(shí)就對(duì)該對(duì)象進(jìn)行訪問(wèn),造成不必要的麻煩。
- 鎖
關(guān)于鎖,我寫過(guò)的一篇。
https://blog.csdn.net/qq_41946557/article/details/101098759
總結(jié)
以上是生活随笔為你收集整理的无招胜有招之Java进阶JVM(四)内存模型plus的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 无招胜有招之Java进阶JVM(三)内存
- 下一篇: 无招胜有招之Java进阶JVM(五)垃圾