volatile关键字及JMM模型
開門見山說:
被volatile修飾的共享變量,就具有了以下兩點特性:
volatile的使用:不要將volatile用在getAndOperate場合(這種場合不原子,需要再加鎖),僅僅set或者get的場景是適合volatile的
JMM存儲結構與CPU對應模型:
加入高速緩存后的CPU執行流程:
緩存一致性:
高速緩存的引入很好的解決了處理器與內存之間的速度矛盾。但在多CPU中,每個線程可能會運行在不同的CPU中,并且每個線程都有自己的高速緩存,同一份數據可能會被緩存到多個CPU中,如果在不同CPU中運行的不同線程看到同一份內存的緩存值不一樣,就會存在緩存不一致的問題。例子如下:
線程1: load i from 主存 // i = 0i + 1 // i = 1 線程2: load i from主存 // 因為線程1還沒將i的值寫回主存,所以i還是0i + 1 //i = 1 線程1: save i to 主存 線程2: save i to 主存 //如果兩個線程按照上面的執行流程,那么i最后的值居然是1了。這就是緩存不一致的問題硬件層面解決緩存一致性的方案:
1.總線鎖
2.緩存鎖(利用CPU緩存一致性)
當某個處理器想要更新主存中的變量的值時,如果該變量在CPU的緩存行中,執行寫回主存操作時,CPU通過緩存一致性協議,通知其它處理器使其它處理器上的緩存失效并重新從主存讀取,以此來保證原子性。
常見的協議有:MSI,MESI,MOSI等等.
最常見的就是MESI協議:
MESI高速緩存一致性協議,MESI表示緩存行的四種狀態,M(Modified 被修改的)、E(Exclusive 獨占的)、S(Share 共享的)、I(Invalid 失效的)。
??緩存行為CPU的高速緩存單位,緩存主存中的部分數據,每個緩存行中用2位來表示MESI四種緩存狀態。
- M:當緩存行處于M狀態時,表示主存內容只在本CPU中緩存,緩存內容與主存不一致,內容被修改,在其他CPU需要對該主存內容進一步讀取之前,需先將緩存內容寫回到主存,然后該緩存行狀態變為S。
- E:當緩存行處于E狀態時,表示主存內容只在本CPU中有緩存,緩存內容與主存一致。在其他CPU需要對該主存內容讀取之前,需要將E狀態更改為S。也可以在緩存寫入時,將E狀態改為M。
- S:當緩存行處于S狀態時,表示主存內容可能在多個CPU中有高速緩存,緩存內容與主存一致,并且隨時可被置為I(無效狀態)。
- I:當緩存行處于I狀態時,表示緩存無效。此時CPU需要讀取時,需要去主存讀取。
- 從CPU讀寫角度來說會遵循以下原則:
CPU讀請求:緩存處于M,E,S狀態都可以被讀取,I狀態CPU還能從主存中讀取數據CPU寫請求:緩存處于M,E狀態下才可以被寫,對于S狀態的寫,需要將其他CPU中緩存置于無效才可寫使用總線鎖和緩存鎖后,CPU對于內存的操作大概可以抽象成下面這樣的結構,從而達成緩存一致性效果
緩存一致性協議/總線鎖就能達到一致性,為何還要volatile?
MESI優化帶來了可見性的問題:MESI 協議雖然可以實現緩存的一致性,但是也會存在一些問題。就是各個 CPU 緩存行的狀態是通過消息傳遞來進行的。如果 CPU0 要對一個在緩存中共享的變量進行寫入,首先需要發送一個失效的消息給到其他緩存了該數據的 CPU。并且要等到他們的確認回執。CPU0 在這段時間內都會處于阻塞狀態。
所以為了避免阻塞帶來的資源浪費。在 cpu 中引入了 Store Bufferes。
這種優化會出現兩個問題:
如下例子:
出現亂序的流程圖:
導致可見性的根本原因是緩存和重排序
如何解決重排序帶來的可見性問題?
濃縮成一句話就是:JMM通過使用volatile,synchronized,final等來進一步設置CPU內存屏障,防止 CPU 對內存的亂序訪問來保證共享數據在多線程并行執行下的可見性。比如這個volatile關鍵字會生成一個 Lock 的匯編指令,這個指令其實就相當于實現了一種內存屏障。進一步合理地禁用了緩存和禁用了重排序,所以JMM最核心的價值就是解決了可見性和有序性。
這方面詳解可以去看大佬的文章,我參考了很多。
JMM基于什么建立
JMM主要就是圍繞著如何在并發過程中如何處理原子性、可見性和有序性這3個特征來建立的。
- 1 . 原子性(Atomicity)
Java中,對基本數據類型的讀取和賦值操作是原子性操作,所謂原子性操作就是指這些操作是不可中斷的,要做一定做完,要么就沒有執行。比如
i = 2; j = i; i++; i = i + 1; //復制代碼上面4個操作中,i=2是讀取操作,必定是原子性操作,j=i你以為是原子性操作,其實吧,分為兩步, //一是讀取i的值,然后再賦值給j,這就是2步操作了,稱不上原子操作,i++和i = i + 1其實是等效的, //讀取i的值,加1,再寫回主存,那就是3步操作了。所以上面的舉例中,最后的值可能出現多種情況, //就是因為滿足不了原子性。這么說來,只有簡單的讀取,賦值是原子操作,還只能是用數字賦值,用變量的話還了 //一步讀取變量值的操作。有個例外是,虛擬機規范中允許對64位數據類型(long和double), //分為2次32為的操作來處理,但是最新JDK實現還是實現了原子操作的。 //JMM只實現了基本的原子性,像上面i++那樣的操作,必須借助于synchronized和Lock來保證整塊代碼的原子性 //了。線程在釋放鎖之前,必然會把i的值刷回到主存的。- 2 . 可見性(Visibility)
- 3 . 有序性(Ordering)
JMM 如何解決順序一致性問題
一句話:禁止特定類型的編譯器重排序,同時要求編譯器生成指令時,插入內存屏障來禁止處理器重排序。
為了提高程序的執行性能,編譯器和處理器都會對指令做重排序。所謂的重排序其實就是指執行的指令順序。編譯器的重排序指的是程序編寫的指令在編譯之后,指令可能會產生重排序來優化程序的執行性能。從源代碼到最終執行的指令,可能會經過三種重排序,如下圖。
添加volatile修飾后的代碼:
那么線程1先執行write,線程2再執行multiply。根據happens-before原則,這個過程會滿足以下3類規則:
- 1.程序順序規則:
- 2.volatile規則:
- 3.傳遞性規則:
從內存語義上來看:
當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存 當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效,線程接下來將從主內存中讀取共享變量。Volatile沒能保證原子性
Volatile沒能保證原子性、要是說能保證,也只是對單個volatile變量的讀/寫具有原子性,但是對于類似volatile++這樣的復合操作就無能為力了,如下例子:
public class Test {public volatile int inc = 0;public void increase() {inc++;}public static void main(String[] args) {final Test test = new Test();for(int i=0;i<10;i++){new Thread(){public void run() {for(int j=0;j<1000;j++)test.increase();};}.start();}while(Thread.activeCount()>1) //保證前面的線程都執行完Thread.yield();System.out.println(test.inc);}按道理來說結果是10000,但是運行下很可能是個小于10000的值。
有人可能會說volatile不是保證了可見性啊,一個線程對inc的修改,另外一個線程應該立刻看到啊!這里的操作inc++是個復合操作,包括讀取inc的值,對其自增,然后再寫回主存。假設線程A,讀取了inc的值為10,這時候被阻塞了,因為沒有對變量進行修改,觸發不了volatile規則。線程B此時也讀inc的值,主存里inc的值依舊為10,做自增,然后立刻就被寫回主存了,為11。此時又輪到線程A執行,由于工作內存里保存的是10,所以繼續做自增,再寫回主存,11又被寫了一遍。所以雖然兩個線程執行了兩次increase(),結果卻只加了一次。
有人說,volatile不是會使緩存行無效的嗎?但是這里線程A讀取到線程B也進行操作之前,并沒有修改inc值,所以線程B讀取的時候,還是讀的10。
又有人說,線程B將11寫回主存,不會把線程A的緩存行設為無效嗎?但是線程A的讀取操作已經做過了,只有在做讀取操作時,發現自己緩存行無效,才會去讀主存的值,所以這里線程A只能繼續做自增了。
- 深入理解
- 綜上所述:
Synchronized和 Volatile 的比較
- 1.Synchronized保證內存可見性和操作的原子性
- 2.Volatile只能保證內存可見性
- 3.Volatile不需要加鎖,比Synchronized更輕量級,并不會阻塞線程(volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。)
- 4.volatile標記的變量不會被編譯器優化,而synchronized標記的變量可以被編譯器優化(如編譯器重排序的優化)
- 5.volatile是變量修飾符,僅能用于變量,而synchronized是一個方法或塊的修飾符。
- 6.volatile本質是在告訴JVM當前變量在寄存器中的值是不確定的,使用前,需要先從主存中讀取,因此可以實現可見性。而對n=n+1,n++等操作時,volatile關鍵字將失效,不能起到像synchronized一樣的線程同步(原子性)的效果。
思路
為了CPU處理快-->加上高速緩存區-->出現緩存不一致的問題-->使用緩存一致性協議/總線鎖優化-->為了避免阻塞帶來的資源浪費,又在 cpu 中引入了 Store Bufferes-->Store Bufferes的引入后,因為緩存和重排序又會出現可見性問題-->JMM通過(volatile、Synchronized等)禁止特定類型的編譯器重排序,同時要求編譯器生成指令時,插入內存屏障來禁止處理器重排序->最終通過volatile 關鍵字解決了排序性,可見性兩個問題,卻沒能解決原子性問題,只能通過Synchronized、lock等解決(而Synchronized、lock開銷又比volatile大)........服了本文基于以下大佬文章,再輔以自己的理解:
https://juejin.im/post/5a2b53b7f265da432a7b821c
https://juejin.im/post/5d774dbbf265da03d728445a
https://juejin.im/post/5d9c8ab4518825094e372706
總結
以上是生活随笔為你收集整理的volatile关键字及JMM模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 互联网晚报 | 8月11日 星期三 |
- 下一篇: 线程的虚假唤醒