面試官問我Volatile的原理?那吹唄!誰怕誰!

在多線程併發編程中,synchronized和volatile都扮演着及其重要的角色;可以這麼說,Volatile是輕量級的synchronized!volatile他在多處理器開發中保證了共享變量的可見性!也能保證在多線程併發情況中指令重排序的情況!

什麼是可見性?

電腦處理器爲了提高運行速度,所以不會直接與內存進行交互!而是先會將數據讀取到內部緩存!之後在進行操作,操作完之後滿足一定條件之後,纔會將內部緩存的數據寫進內存!所以,多線程共享變量,可能會存在髒讀的現象,也就是,明明已經將數據更改!但是卻會出現因爲各個處理器內部緩存沒有更新,所導致的髒讀現象!volatile的存在就是爲了解決這個問題!使用了volatile聲明的變量會將這個數據在緩存行的數據寫入到內存中!同時各個處理器通過嗅探在總線上傳播的數據來檢查自己的緩存是不是過期了!進而保證數據對於各個線程和處理器的可見性!

那麼,volatile是如何保證可見性的呢?

我們先看一段代碼!

public class TestVolatile{
    public static volatile int value;
    public static void main(String[] args) {
           int a = 10;
           value = 9;
           value += a;
    }
}

我們知道,java代碼在編譯後,會被編譯成字節碼!最終字節碼被類加載器加載到JVM裏面!JVM執行字節碼最終也需要轉換爲彙編指令在CPU上運行!那麼我們就將這段代碼編譯爲彙編語言,看一下volatile修飾的變量,到底做了什麼操作!保證了可見性!

 0x00007f96b93132ed: lock addl $0x0,(%rsp)     ;*putstatic value
                                                ; - TestVolatile::main@5 (line 6)

重點關注lock addl $0x0,(%rsp)通過查驗 IA-32架構軟件開發人員手冊 發現,帶有lock前綴的的指令在多核處理器會發生兩件事:

  1. 將當前處理器緩存行的數據寫回到系統內存裏面去
  2. 這個寫回內存的操作會使其他CPU緩存行的數據無效

所以說在這個數據進行修改操作的時候,會重新從系統內存中把數據讀取到緩存行中!

volatile的定義:在java語言規範第三版中對volatile的定義如下,Java編程語言,允許線程訪問共享變量,爲了確保共享變量能夠被準確和一致的更新,線程應該使用排他鎖來單獨獲取這個變量!

Lock前綴指令導致在執行指令期間,聲言處理器的LOCK#信號。在多處理器環境中,LOCK#信號確保在聲言該信號期間,處理器可以獨佔任何共享內存!

爲什麼 處理器可以獨佔任何共享內存呢?

因爲它會鎖住總線,導致其他CUP不能訪問總線,不能訪問總線就意味着不能訪問系統內存!

總線鎖定把CPU和內存的通信給鎖住了,使得在鎖定期間,其他處理器不能操作其他內存地址的數據,從而開銷較大,所以後來的CPU都提供了緩存一致性機制,Intel的奔騰486之後就提供了這種優化。

緩存一致性:緩存一致性機制就整體來說,是當某塊CPU對緩存中的數據進行操作了之後,就通知其他CPU放棄儲存在它們內部的緩存,或者從主內存中重新讀取,用MESI闡述原理如下:

MESI協議:是以緩存行(緩存的基本數據單位,在Intel的CPU上一般是64字節)的幾個狀態來命名的(全名是Modified、Exclusive、 Share or Invalid)。該協議要求在每個緩存行上維護兩個狀態位,使得每個數據單位可能處於M、E、S和I這四種狀態之一,各種狀態含義如下:

  • M:被修改的。處於這一狀態的數據,只在本CPU中有緩存數據,而其他CPU中沒有。同時其狀態相對於內存中的值來說,是已經被修改的,且沒有更新到內存中。

  • E:獨佔的。處於這一狀態的數據,只有在本CPU中有緩存,且其數據沒有修改,即與內存中一致。

  • S:共享的。處於這一狀態的數據在多個CPU中都有緩存,且與內存一致。

  • I:無效的。本CPU中的這份緩存已經無效。

​ 一個處於M狀態的緩存行,必須時刻監聽所有試圖讀取該緩存行對應的主存地址的操作,如果監聽到,則必須在此操作執行前把其緩存行中的數據寫回CPU。
​ 一個處於S狀態的緩存行,必須時刻監聽使該緩存行無效或者獨享該緩存行的請求,如果監聽到,則必須把其緩存行狀態設置爲I。
​ 一個處於E狀態的緩存行,必須時刻監聽其他試圖讀取該緩存行對應的主存地址的操作,如果監聽到,則必須把其緩存行狀態設置爲S。

​ 當CPU需要讀取數據時,如果其緩存行的狀態是I的,則需要從內存中讀取,並把自己狀態變成S,如果不是I,則可以直接讀取緩存中的值,但在此之前,必須要等待其他CPU的監聽結果,如其他CPU也有該數據的緩存且狀態是M,則需要等待其把緩存更新到內存之後,再讀取。

​ 當CPU需要寫數據時,只有在其緩存行是M或者E的時候才能執行,否則需要發出特殊的RFO指令(Read Or Ownership,這是一種總線事務),通知其他CPU置緩存無效(I),這種情況下性能開銷是相對較大的。在寫入完成後,修改其緩存狀態爲M。

​ 所以如果一個變量在某段時間只被一個線程頻繁地修改,則使用其內部緩存就完全可以辦到,不涉及到總線事務,如果緩存一會被這個CPU獨佔、一會被那個CPU 獨佔,這時纔會不斷產生RFO指令影響到併發性能。

其實JDK7的併發包中,著名的併發編程大師,Doug lea 新增了一個隊列集合 Linked-TransferQueue 他用了一種特殊的方式優化了volatile,是一種追加字節的方式!我們以後可能會出一個詳解的,想要探究他,就一定要探究到處理器的硬件配置!我們有時間再說!

關於可見性的一個小案例

public class NoVisibility {
    private static boolean ready;
    private static class ReaderThread extends Thread{
        public void run(){
            while (!ready) {
                System.out.println(3);
            }
            System.out.println("-------------我是咋執行的??-----------------");
        }
    }
    public static void main(String args[]) throws Exception{
        new ReaderThread().start();
        ready=true;
    }
}

對於上面的一個代碼,正常情況下,他應該一直輸出3,但是如果發生髒讀的情況!也就是緩存行的數據沒有更新,那麼有可能執行這個代碼:

System.out.println("-------------我是咋執行的??-----------------");

什麼是指令重排序

在執行程序時,爲了提高性能,編譯器和處理器常常會對指令做重排序。

  1. 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。

  2. 指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-LevelParallelism,ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。

  3. 內存系統的重排序。由於處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。

volatile是如何防止指令重排序的?

從彙編語言中可以看到在對volatile變量賦值後會加一條lock addl $0x0,(%rsp)指令;lock指令具有內存屏障的作用,lock前後的指令不會重排序;

**內存屏障:**CPU術語定義是一組處理器指令,用於實現對內存操作的順序限制!

在hotspot源碼中內存屏障也是使用這樣的指令實現的,沒使用mfence指令,hotspot中解釋說mfence有時候開銷會很大。

內存屏障的功能,java解釋器遇到volatile變量,會在volatile變量賦值之後,加一個lock addl $0x0,(%rsp)具有內存屏障功能的指令,防止內存重排序。

可能咋這麼說不太好理解,我們舉個例子來說明一下:

package com.zanzan.test;

public class TestVolatile {
    int a = 0;
    boolean flag = false;

    public void testA(){
        //語句1
        a = 1;
        //語句2
        flag = true;
    }

    public void testB(){
        if (flag){
            a = a + 5;
            System.out.println(a);
        }
    }

    public static void main(String[] args) {
        TestVolatile testVolatile = new TestVolatile();
        new Thread(new Runnable() {
            @Override
            public void run() {
                testVolatile.testA();
            }
        },"testVolatileA").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testVolatile.testB();
            }
        },"testVolatileB").start();
    }

}

正常情況下結果是:6

但是發生指令重排後,語句2先執行,執行後線程時間片切換;線程2執行testB(),此時a = 0 那麼此時結果爲 :5

這就是指令重排序!

我們使用 volatile就可以解決:如何解決呢?

volatile boolean  flag = false;
volatile int a = 0;

如果你覺的還可以!歡迎關注作者哦! 公衆號:【JAVA程序狗】
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章