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