91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何理解Java中volatile關鍵字

發布時間:2021-10-09 14:39:00 來源:億速云 閱讀:118 作者:iii 欄目:編程語言

這篇文章主要介紹“如何理解Java中volatile關鍵字”,在日常操作中,相信很多人在如何理解Java中volatile關鍵字問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何理解Java中volatile關鍵字”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

CPU緩存

緩存模型

計算機中的所有運算操作都是由CPU完成的,CPU指令執行過程需要涉及數據讀取和寫入操作,但是CPU只能訪問處于內存中的數據,而內存的速度和CPU的速度是遠遠不對等的,因此就出現了緩存模型,也就是在CPU和內存之間加入了緩存層。一般現代的CPU緩存層分為三級,分別叫L1緩存、L2緩存和L3緩存,簡略圖如下:

如何理解Java中volatile關鍵字

  • L1緩存:三級緩存中訪問速度最快,但是容量最小,另外L1緩存還被劃分成了數據緩存(L1ddata首字母)和指令緩存(L1iinstruction首字母)

  • L2緩存:速度比L1慢,但是容量比L1大,在現代的多核CPU中,L2一般被單個核獨占

  • L3緩存:三級緩存中速度最慢,但是容量最大,現代CPU中也有L3是多核共享的設計,比如zen3架構的設計

如何理解Java中volatile關鍵字

緩存的出現,是為了解決CPU直接訪問內存效率低下的問題,CPU進行運算的時候,將需要的數據從主存復制一份到緩存中,因為緩存的訪問速度快于內存,在計算的時候只需要讀取緩存并將結果更新到緩存,運算結束再將結果刷新到主存,這樣就大大提高了計算效率,整體交互圖簡略如下:

如何理解Java中volatile關鍵字

緩存一致性問題

雖然緩存的出現,大大提高了吞吐能力,但是,也引入了一個新的問題,就是緩存不一致。比如,最簡單的一個i++操作,需要將內存數據復制一份到緩存中,CPU讀取緩存值并進行更新,先寫入緩存,運算結束后再將緩存中新的刷新到內存,具體過程如下:

  • 讀取內存中的i到緩存中

  • CPU讀取緩存i中的值

  • i進行加1操作

  • 將結果寫回緩存

  • 再將數據刷新到主存

這樣的i++操作在單線程不會出現問題,但在多線程中,因為每個線程都有自己的工作內存(也叫本地內存,是線程自己的緩存),變量i在多個線程的本地內存中都存在一個副本,如果有兩個線程執行i++操作:

  • 假設兩個線程為A、B,同時假設i初始值為0

  • 線程A從內存中讀取i的值放入緩存中,此時i的值為0,線程B也同理,放入緩存中的值也是0

  • 兩個線程同時進行自增操作,此時A、B線程的緩存中,i的值都是1

  • 兩個線程將i寫入主內存,相當于i被兩次賦值為1

  • 最終結果是i的值為1

這個就是典型的緩存不一致問題,主流的解決辦法有:

  • 總線加鎖

  • 緩存一致性協議

總線加鎖

這是一種悲觀的實現方式,具體來說,就是通過處理器發出lock指令,鎖住總線,總線收到指令后,會阻塞其他處理器的請求,直到占用鎖的處理器完成操作。特點是只有一個搶到總線鎖的處理器運行,但是這種方式效率低下,一旦某個處理器獲取到鎖其他處理器只能阻塞等待,會影響多核處理器的性能。

緩存一致性協議

圖示如下:

如何理解Java中volatile關鍵字

緩存一致性協議中最出名的就是MESI協議,MESI保證了每一個緩存中使用的共享變量的副本都是一致的。大致思想是,CPU操作緩存中的數據時,如果發現該變量是一個共享變量,操作如下:

  • 讀取:不做其他處理,只是將緩存中數據讀取到寄存器中

  • 寫入:發出信號通知其他CPU將該變量的緩存行設置為無效狀態(Invalid),其他CPU進行該變量的讀取時需要到主存中再次獲取

具體來說,MESI中規定了緩存行使用4種狀態標記:

  • MModified,被修改

  • EExclusive,獨享的

  • SShared,共享的

  • IInvalid,無效的

有關MESI詳細的實現超出了本文的范圍,想要詳細了解可以參考此處或此處。

JMM

看完了CPU緩存再來看一下JMM,也就是Java內存模型,指定了JVM如何與計算機的主存進行工作,同時也決定了一個線程對共享變量的寫入何時對其他線程可見,JMM定義了線程和主內存之間的抽象關系,具體如下:

  • 共享變量存儲于主內存中,每個線程都可以訪問

  • 每個線程都有私有的工作內存或者叫本地內存

  • 工作內存只存儲該線程對共享變量的副本

  • 線程不能直接操作主內存,只有先操作了工作內存之后才能寫入主內存

  • 工作內存和JMM內存模型一樣也是一個抽象概念,其實并不存在,涵蓋了緩存、寄存器、編譯期優化以及硬件等

簡略圖如下:

如何理解Java中volatile關鍵字

MESI類似,如果一個線程修改了共享變量,刷新到主內存后,其他線程讀取工作內存的時候發現緩存失效,會從主內存再次讀取到工作內存中。

而下圖表示了JVM與計算機硬件分配的關系:

如何理解Java中volatile關鍵字

并發編程的三個特性

文章都看了大半了還沒到volatile?別急別急,先來看看并發編程中的三個重要特性,這對正確理解volatile有很大的幫助。

原子性

原子性就是在一次或多次操作中:

  • 要么所有的操作全部都得到了執行,且不會受到任何因素的干擾而中斷

  • 要么所有的操作都不執行

一個典型的例子就是兩個人轉賬,比如A向B轉賬1000元,那么這包含兩個基本的操作:

  • A的賬戶扣除1000元

  • B的賬戶增加1000元

這兩個操作,要么都成功,要么都失敗,也就是不能出現A賬戶扣除1000但是B賬戶金額不變的情況,也不能出現A賬戶金額不變B賬戶增加1000的情況。

需要注意的是兩個原子性操作結合在一起未必是原子性的,比如i++。本質上來說,i++涉及到了三個操作:

  • get i

  • i+1

  • set i

這三個操作都是原子性的,但是組合在一起(i++)就不是原子性的。

可見性

另一個重要的特性是可見性,可見性是指,一個線程對共享變量進行了修改,那么另外的線程可以立即看到修改后的最新值。

一個簡單的例子如下:

public class Main {
    private int x = 0;
    private static final int MAX = 100000;
    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        Thread thread0 = new Thread(()->{
            while(m.x < MAX) {
                ++m.x;
            }
        });

        Thread thread1 = new Thread(()->{
            while(m.x < MAX){
            }
            System.out.println("finish");
        });

        thread1.start();
        TimeUnit.MILLISECONDS.sleep(1);
        thread0.start();
    }
}

線程thread1會一直運行,因為thread1x讀入工作內存后,會一直判斷工作內存中的值,由于thread0改變的是thread0工作內存的值,并沒有對thread1可見,因此永遠也不會輸出finish,使用jstack也可以看到結果:

如何理解Java中volatile關鍵字

有序性

有序性是指代碼在執行過程中的先后順序,由于JVM的優化,導致了代碼的編寫順序未必是代碼的運行順序,比如下面的四條語句:

int x = 10;
int y = 0;
x++;
y = 20;

有可能y=20x++前執行,這就是指令重排序。一般來說,處理器為了提高程序的效率,可能會對輸入的代碼指令做一定的優化,不會嚴格按照編寫順序去執行代碼,但可以保證最終運算結果是編碼時的期望結果,當然,重排序也有一定的規則,需要嚴格遵守指令之間的數據依賴關系,并不是可以任意重排序,比如:

int x = 10;
int y = 0;
x++;
y = x+1;

y=x+1就不能先優于x++執行。

在單線程下重排序不會導致預期值的改變,但在多線程下,如果有序性得不到保證,那么將可能出現很大的問題:

private boolean initialized = false;
private Context context;
public Context load(){
    if(!initialized){
        context = loadContext();
        initialized = true;
    }
    return context;
}

如果發生了重排序,initialized=true排序到了context=loadContext()的前面,假設兩個線程A、B同時訪問,且loadContext()需要一定耗時,那么:

  • 線程A通過判斷后,先設置布爾變量的值為true,再進行loadContext()操作

  • 線程B中由于布爾變量被設置為true,會直接返回一個未加載完成的context

volatile

好了終于到了volatile了,前面說了這么多,目的就是為了能徹底理解和明白volatile。這部分分為四個小節:

  • volatile的語義

  • 如何保證有序性以及可見性

  • 實現原理

  • 使用場景

  • synchronized區別

先來介紹一下volatile的語義。

語義

volatile修飾的實例變量或者類變量具有兩層語義:

  • 保證了不同線程之間對共享變量操作時的可見性

  • 禁止對指令進行重排序操作

如何保證可見性以及有序性

先說結論:

  • volatile能保證可見性

  • volatile能保證有序性

  • volatile不能保證原子性

下面分別進行介紹。

可見性

Java中保證可見性有如下方式:

  • volatile:當一個變量被volatile修飾時,對共享資源的讀操作會直接在主內存中進行(準確來說也會讀取到工作內存中,但是如果其他線程進行了修改就必須從主內存重新讀取),寫操作是先修改工作內存,但是修改結束后立即刷新到主內存中

  • synchronizedsynchronized一樣能保證可見性,能夠保證同一時刻只有一個線程獲取到鎖,然后執行同步方法,并且確保鎖釋放之前,變量的修改被刷新到主內存中

  • 使用顯式鎖LockLocklock方法能保證同一時刻只有一個線程能夠獲取到鎖然后執行同步方法,并且確保鎖釋放之前能夠將對變量的修改刷新到主內存中

具體來說,可以看一下之前的例子:

public class Main {
    private int x = 0;
    private static final int MAX = 100000;
    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        Thread thread0 = new Thread(()->{
            while(m.x < MAX) {
                ++m.x;
            }
        });

        Thread thread1 = new Thread(()->{
            while(m.x < MAX){
            }
            System.out.println("finish");
        });

        thread1.start();
        TimeUnit.MILLISECONDS.sleep(1);
        thread0.start();
    }
}

上面說過這段代碼會不斷運行,一直沒有輸出,就是因為修改后的x對線程thread1不可見,如果在x的定義中加上了volatile,就不會出現沒有輸出的情況了,因為此時對x的修改是線程thread1可見的。有序性

JMM中允許編譯期和處理器對指令進行重排序,在多線程的情況下有可能會出現問題,為此,Java同樣提供了三種機制去保證有序性:

  • volatile

  • synchronized

  • 顯式鎖Lock

另外,關于有序性不得不提的就是Happens-before原則。Happends-before原則說的就是如果兩個操作的執行次序無法從該原則推導出來,那么就無法保證有序性,JVM或處理器可以任意重排序。這么做的目的是為了盡可能提高程序的并行度,具體規則如下:

  • 程序次序規則:在一個線程內,代碼按照編寫時的次序執行,編寫在后面的操作發生與編寫在前面的操作之后

  • 鎖定規則:如果一個鎖處于鎖定狀態,則unlock操作要先行發生于對同一個鎖的lock操作

  • volatile變量規則:對一個變量的寫操作要早于對這個變量之后的讀操作

  • 傳遞規則:如果操作A先于操作B,操作B先于操作C,那么操作A先于操作C

  • 線程啟動規則:Thread對象的start()方法先行發生于對該線程的任何動作

  • 線程中斷規則:對線程執行interrupt()方法肯定要優于捕獲到中斷信號,換句話說,如果收到了中斷信號,那么在此之前必定調用了interrupt()

  • 線程終結規則:線程中所有操作都要先行發生于線程的終止檢測,也就是邏輯單元的執行肯定要發生于線程終止之前

  • 對象終結規則:一個對象初始化的完成先行發生于finalize()之前

對于volatile,會直接禁止對指令重排,但是對于volatile前后無依賴關系的指令可以隨意重排,比如:

int x = 0;
int y = 1;
//private volatile int z;
z = 20;
x++;
y--;

z=20之前,先定義x或先定義y并沒有要求,只需要在執行z=20的時候,可以保證x=0,y=1即可,同理,x++y--具體先執行哪一個并沒有要求,只需要保證兩者執行在z=20之后即可。

原子性

Java中,所有對基本數據類型變量的讀取賦值操作都是原子性的,對引用類型的變量讀取和賦值也是原子性的,但是:

  • 將一個變量賦值給另一個變量的操作不是原子性的,因為涉及到了一個變量的讀取以及一個變量的寫入,兩個原子性操作結合在一起就不是原子性操作

  • 多個原子性操作在一起就不是原子性操作,比如i++

  • JMM只保證基本讀取和賦值的原子性操作,其他的均不保證,如果需要具備原子性,那么可以使用synchronizedLock,或者JUC包下的原子操作類

也就是說,volatile并不能保證原子性,例子如下:

public class Main {
    private volatile int x = 0;
    private static final CountDownLatch latch = new CountDownLatch(10);

    public void inc() {
        ++x;
    }

    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        IntStream.range(0, 10).forEach(i -> {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    m.inc();
                }
                latch.countDown();
            }).start();
        });
        latch.await();
        System.out.println(m.x);
    }
}

最后輸出的x的值會少于10000,而且每次運行的結果也并不相同,至于原因,可以從兩個線程A、B開始分析,圖示如下:

如何理解Java中volatile關鍵字

  • 0-t1:線程A將x讀入工作內存,此時x=0

  • t1-t2:線程A時間片完,CPU調度線程B,線程B將x讀入工作內存,此時x=0

  • t2-t3:線程B對工作內存中的x進行自增操作,并更新到工作內存中

  • t3-t4:線程B時間片完,CPU調度線程A,同理線程A對工作內存中的x自增

  • t4-t5:線程A將工作內存中的值寫回主內存,此時主內存中的值為x=1

  • t5以后:線程A時間片完,CPU調度線程B,線程B也將自己的工作內存寫回主內存,再次將主內存中的x賦值為1

也就是說,多線程操作的話,會出現兩次自增但是實際上只進行一次數值修改的操作。想要x的值變為10000也很簡單,加上synchronized即可:

new Thread(() -> {
    synchronized (m) {
        for (int j = 0; j < 1000; j++) {
            m.inc();
        }
    }
    latch.countDown();
}).start();

實現原理

前面已經知道,volatile可以保證有序性以及可見性,那么,具體是如何操作的呢?

答案就是一個lock;前綴,該前綴實際上相當于一個內存屏障,該內存屏障會為指令的執行提供如下幾個保障:

  • 確保指令重排序時不會將其后面的代碼排到內存屏障之前

  • 確保指令重排序時不會將其前面的代碼排到內存屏障之后

  • 確保執行到內存屏障修飾的指令時前面的代碼全部執行完成

  • 強制將線程工作內存中的值修改刷新到主存中

  • 如果是寫操作,會導致其他線程工作內存中的緩存數據失效

使用場景

一個典型的使用場景是利用開關進行線程的關閉操作,例子如下:

public class ThreadTest extends Thread{
    private volatile boolean started = true;

    @Override
    public void run() {
        while (started){
            
        }
    }

    public void shutdown(){
        this.started = false;
    }
}

如果布爾變量沒有被volatile修飾,那么很可能新的布爾值刷新不到主內存中,導致線程不會結束。

synchronized的區別

  • 使用上的區別:volatile只能用于修飾實例變量或者類變量,但是不能用于修飾方法、方法參數、局部變量等,另外可以修飾的變量為null。但synchronized不能用于對變量的修飾,只能修飾方法或語句塊,而且monitor對象不能為null

  • 對原子性的保證:volatile無法保證原子性,但是synchronized可以保證

  • 對可見性的保證:volatilesynchronized都能保證可見性,但是synchronized是借助于JVM指令monitor enter/monitor exit保證的,在monitor exit的時候所有共享資源都被刷新到主內存中,而volatile是通過lock;機器指令實現的,迫使其他線程工作內存失效,需要到主內存加載

  • 對有序性的保證:volatile能夠禁止JVM以及處理器對其進行重排序,而synchronized保證的有序性是通過程序串行化執行換來的,并且在synchronized代碼塊中的代碼也會發生指令重排的情況

  • 其他區別:volatile不會使線程陷入阻塞,但synchronized

到此,關于“如何理解Java中volatile關鍵字”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

岳池县| 寿阳县| 江门市| 石家庄市| 辽源市| 高雄县| 寻乌县| 泰和县| 奎屯市| 德庆县| 安平县| 郓城县| 托里县| 五原县| 恩平市| 公安县| 洪雅县| 宜州市| 米林县| 临洮县| 北川| 独山县| 长寿区| 北流市| 鄄城县| 白城市| 青岛市| 灌南县| 洪洞县| 封开县| 泗洪县| 山东| 尤溪县| 越西县| 铜山县| 子洲县| 泰顺县| 丘北县| 车致| 金山区| 蒙阴县|