日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

volatile关键字及JMM模型

發布時間:2025/3/15 编程问答 20 豆豆
生活随笔 收集整理的這篇文章主要介紹了 volatile关键字及JMM模型 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

開門見山說:
被volatile修飾的共享變量,就具有了以下兩點特性:

1 .保證了不同線程對該變量操作的內存可見性;可見性: 在多線程情況下,讀和寫發生在不同的線程中,而讀線程未能及時的讀到寫線程寫入的最新的值導致可見性的根本原因是就是緩存和重排序2 .禁止指令重排序重排序:其實就是指執行的指令順序重新排序(不是按代碼順序)

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。

這種優化會出現兩個問題:

  • 數據什么時候提交是不確定的,因為需要等待其他 cpu給回復才會進行數據同步。這里其實是一個異步操作
  • 引入了 storebufferes 后,處理器會先嘗試從 storebuffer中讀取值,如果 storebuffer 中有數據,則直接從storebuffer 中讀取,否則就再從緩存行中讀取
    如下例子:
  • int a = 0; bool flag = false; public void write() {a = 2; //1flag = true; //2 } public void multiply() {if (flag) { //3int ret = a * a;//4} }

    出現亂序的流程圖:

    導致可見性的根本原因是緩存和重排序

    如何解決重排序帶來的可見性問題?

    濃縮成一句話就是: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)
    說到可見性,Java就是利用volatile來提供可見性的。 當一個變量被volatile修飾時,那么對它的修改會立刻刷新到主存, 當其它線程需要讀取該變量時,會去內存中讀取新值。而普通變量則不能保證這一點。 其實通過synchronized和Lock也能夠保證可見性,線程在釋放鎖之前,會把共享變量值都刷回主存, 但是synchronized和Lock的開銷都更大。
    • 3 . 有序性(Ordering)
    JMM是允許編譯器和處理器對指令重排序的,但是規定了as-if-serial語義,即不管怎么重排序, 程序的執行結果不能改變。比如下面的程序段: double pi = 3.14; //A double r = 1; //B double s= pi * r * r; //C 復制代碼上面的語句,可以按照A->B->C執行,結果為3.14,但是也可以按照B->A->C的順序執行, 因為A、B是兩句獨立的語句,而C則依賴于A、B,所以A、B可以重排序, 但是C卻不能排到A、B的前面。JMM保證了重排序不會影響到單線程的執行,但是在多線程中卻容易出問題。

    JMM 如何解決順序一致性問題

    一句話:禁止特定類型的編譯器重排序,同時要求編譯器生成指令時,插入內存屏障來禁止處理器重排序。
    為了提高程序的執行性能,編譯器和處理器都會對指令做重排序。所謂的重排序其實就是指執行的指令順序。編譯器的重排序指的是程序編寫的指令在編譯之后,指令可能會產生重排序來優化程序的執行性能。從源代碼到最終執行的指令,可能會經過三種重排序,如下圖。

    添加volatile修飾后的代碼:

    int a = 0; volatile bool flag = false; public void write() {a = 2; //1flag = true; //2 } public void multiply() {if (flag) { //3int ret = a * a;//4} }

    那么線程1先執行write,線程2再執行multiply。根據happens-before原則,這個過程會滿足以下3類規則:

    • 1.程序順序規則:
    1 happens-before 2; 3 happens-before 4; (volatile限制了指令重排序,所以12 之前執行)
    • 2.volatile規則:
    2 happens-before 3
    • 3.傳遞性規則:
    1 happens-before 4

    從內存語義上來看:

    當寫一個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只能繼續做自增了。

    • 深入理解
    由上面例子 1.從主內存取出x到線程工作內存; 2.臨時變量 t = x+13.x = t;因為x更改,所以立刻將 x 寫到主存,使其他線程的工作內存失效。(這一步是不可拆分的)x初始值為10,線程1執行完第二步,尚未執行第三步時,切換至線程2,此時x依然為10,線程2執行完三步之后, x變更為11,因為x更改,線程1的工作內存立刻失效,此時切換到線程1,線程1只會繼續執行第三步, t 之前已經被計算出來是11,直接賦值給x,因此x最終還是11。 之所以會這樣,就是因為這不是一步,而是三步,也就是非原子的。volatile保證了x的更改立刻被線程知曉, 但無法回退 “因為x的改變,需要重新執行” 的步驟。
    • 綜上所述:
    在這種復合操作的情景下,原子性的功能是維持不了了。但是volatile在上面那種設置flag值的例子里,由于對flag的讀/寫操作都是單步的,所以還是能保證原子性的。 要想保證原子性,只能借助于synchronized,Lock以及并發包下的atomic的原子操作類了,即對基本數據類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個數),減法操作(減一個數)進行了封裝,保證這些操作是原子性操作。

    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模型的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。