天天看點

第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則

CPU内部結構劃分
控制單元
運算單元
存儲單元

        
第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則
計算機多硬體多CPU結構:

        
第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則

CPU緩存一緻性原則

第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則
JMM同步八種操作介紹:
(1)lock(鎖定):作用于主記憶體的變量,把一個變量标記為一條線程獨占狀态 
(2)unlock(解鎖):作用于主記憶體的變量,把一個處于鎖定狀态的變量釋放出來,釋放後的 變量才可以被其他線程鎖定
(3)read(讀取):作用于主記憶體的變量,把一個變量值從主記憶體傳輸到線程的工作記憶體中, 以便随後的load動作使用
(4)load(載入):作用于工作記憶體的變量,它把read操作從主記憶體中得到的變量值放入工作 記憶體的變量副本中
(5)use(使用):作用于工作記憶體的變量,把工作記憶體中的一個變量值傳遞給執行引擎 
(6)assign(指派):作用于工作記憶體的變量,它把一個從執行引擎接收到的值賦給工作記憶體的變量 
(7)store(存儲):作用于工作記憶體的變量,把工作記憶體中的一個變量的值傳送到主記憶體中,以便随後的write的操作 
(8)write(寫入):作用于工作記憶體的變量,它把store操作從工作記憶體中的一個變量的值傳送 到主記憶體的變量中 
如果要把一個變量從主記憶體中複制到工作記憶體中,就需要按順序地執行read和load操作, 如果把變量從工作記憶體中同步到主記憶體中,
就需要按順序地執行store和write操作。但Java内 存模型隻要求上述操作必須按順序執行,而沒有保證必須是連續執行。

        
第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則
JMM三大特性
原子性
  彙編指令 --原子比較和交換在底層的支援 cmp-chxg
   解決辦法: Synchronized  Lock鎖機制  保證任意時刻隻有一個線程通路該代碼塊。      
public class VolatileAtomicSample {

    private static volatile int counter = 0; // volatile無法保證原子性
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    counter++; //不是一個原子操作,第一輪循環結果是沒有刷入主存,這一輪循環已經無效
                    //1 load counter 到工作記憶體
                    //2 add counter 執行自加
                }
            });
            thread.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}      

啟動10個線程,每個線程執行自增步驟,count++ 是非原子性的。volatile保證資料的可見性,同時存在CPU緩存鎖機制以及MESI緩存分布式協定,最後列印的值 <= 10000.

CPU為了提升性能,會存在指令編排機制。也就會出現記憶體屏障  見有序性詳解。

可見性  volatile  -- LOCK緩存行(有且僅有一個線程會占有緩存行) + CPU緩存一緻性原則MESI(獨占E-->共享S-->修改M--->其他失效I)      
public class VolatileVisibilitySample {
    private boolean initFlag = false;
    public void refresh(){
        this.initFlag = true; //普通寫操作,(volatile寫)
        String threadname = Thread.currentThread().getName();
        System.out.println("線程:"+threadname+":修改共享變量initFlag");
    }

    public void load(){
        String threadname = Thread.currentThread().getName();
        int i = 0;
        while (!initFlag){
            i++;
        }
        System.out.println("線程:"+threadname+"目前線程嗅探到initFlag的狀态的改變"+i);
    }
    public static void main(String[] args){
        VolatileVisibilitySample sample = new VolatileVisibilitySample();
        Thread threadA = new Thread(()->{
            sample.refresh();
        },"threadA");
        Thread threadB = new Thread(()->{
            sample.load();
        },"threadB");
        threadB.start();
        try {
             Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.start();
    }
}      

分析如下: 隻會列印 "線程:threadA:修改共享變量initFlag". 

第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則

修改:

第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則

因為線上程A裡面增加了鎖機制,同時CPU自身也存在時間片切片,導緻線程上下文切換,initFlag會從記憶體中讀取線程B更新的值。

會把線程B嗅探機制列印出來。列印如下:

線程:threadA:修改共享變量initFlag

線程:threadB目前線程嗅探到initFlag的狀态的改變25747425

修改2:使用volatile關鍵字  ====> JMM緩存一緻性原則(MESI) + LOCK緩存行

第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則
有序性 
  -- 指令重排  ---> 記憶體屏障(volatile禁止重排優化 )      
檢視彙編指令:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp


as-if-serial語義的意思是:不管怎麼重排序(編譯器和處理器為了提高并行度),(單線程)。 即在單線程情況下,不能改變程式運作的結果      

             程式的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。

double p = 3.14; //1
double r = 1.0; //2
double area = p * r * r; //3計算面積      

上面例子中1,2存在指令重排操作,但是1,2不能和第三步存在指令重排操作,否則将改變程式運作的結果。

happen-before原則

1、 程式順序原則,即在一個線程内必須保證語義串行性,也就是說按照代碼順序執行      
2. 鎖規則 解鎖(unlock)操作必然發生在後續的同一個鎖的加鎖(lock)之前,也就是說,如果對于一個鎖解鎖後,再加鎖,那麼加鎖的動作必須在解鎖動作之後(同一個鎖)
      
3. volatile規則 volatile變量的寫,先發生于讀,這保證了volatile變量的可見性,簡單的了解就是,volatile變量在每次被線程通路時,都強迫從主記憶體中讀該變量的值,
   而當該變量發生變化時,又會強迫将最新的值重新整理到主記憶體,任何時刻,不同的線程總是能夠看到該變量的最新值。      
4. 線程啟動原則 線程的start()方法先于他的每一個動作,即如果線程A在執行線程B的start方法之前修改了共享變量的值,那麼當線程B執行了start方法之時,線程A對共享變量的修改對線程B可見。      
5. 傳遞性  A先于B,B先于C,那麼A必然先于C      
6. 線程終止原則  線程的所有操作先于線程的終結。Thread.join()方法的作用就是等待目前執行的線程的終止。
   假設線上程B終止之前,修改了共享變量,線程A從線程B的join方法成功傳回後,線程B對共享變量的修改将對線程A可見。      
7. 線程中斷規則      
對線程 interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生      
8、對象終結規則 對象的構造函數執行,結束先于finalize()方法


指令重排發生在什麼階段?
1. 編譯階段,位元組碼編譯成機器指令碼階段。
2. CPU運作時,執行指令

      
volatile禁止重排優化 ---記憶體屏障(Memory Barrier)
下圖是JMM針對編譯器制定的volatile重排序規則表      
指令重排code示例      
/**
 * 并發場景下存在指令重排
 */
public class VolatileReOrderSample {
    private static int x = 0, y = 0;
    private static volatile int a = 0, b =0;
    static Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        int i = 0;

        for (;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(new Runnable() {
                public void run() {
                    //由于線程one先啟動,下面這句話讓它等一等線程two. 讀着可根據自己電腦的實際性能适當調整等待時間.
                    shortWait(10000);
                    a = 1; //是讀還是寫?store,volatile寫
                    //storeload ,讀寫屏障,不允許volatile寫與第二步volatile讀發生重排
                    x = b; // 讀還是寫?讀寫都有,先讀volatile,寫普通變量
                    //分兩步進行,第一步先volatile讀,第二步再普通寫
                }
            }, "t1");
            Thread t2 = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    UnsafeInstance.reflectGetUnsafe().storeFence();
                    y = a;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();

            /**
             * cpu或者jit對我們的代碼進行了指令重排?
             * 1,1
             * 0,1
             * 1,0
             * 0,0
             */
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }

    }

    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }

}      
如果不要volatile去增加記憶體屏障?如何解決?
-- 手動增加屏障,通過Unsafe來解決.      
loadFence() storeFence  fulFence() .
      
Unsafe通過BootStwp被加載,否則抛異常。JVM的雙親委派機制
通過反射來擷取。      
public class UnsafeInstance {

    public static Unsafe reflectGetUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}      
第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則
記憶體屏障 Memory Barrier      
1.寫寫storestore 2.寫讀storeload  3.讀寫loadstore 4.讀讀loadload       
第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則
volatile禁止重排優化 
volatile關鍵字另一個作用就是禁止指令重排優化,進而避免多線程環境下程式出現亂序 執行的現象,關于指令重排優化前面已詳細分析過,這裡主要簡單說明一下volatile是如何實 現禁止指令重排優化的。先了解一個概念,記憶體屏障(Memory Barrier)。  
記憶體屏障,又稱記憶體栅欄,是一個CPU指令,它的作用有兩個,一是保證特定操作的執行 順序,二是保證某些變量的記憶體可見性(利用該特性實作volatile的記憶體可見性)。由于編譯 器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴編譯器 和CPU,
不管什麼指令都不能和這條Memory Barrier指令重排序,也就是說通過插入記憶體屏 障禁止在記憶體屏障前後的指令執行重排序優化。Memory Barrier的另外一個作用是強制刷出 各種CPU的緩存資料,是以任何CPU上的線程都能讀取到這些資料的新版本。總之, volatile變量正是通過記憶體屏障實作其在記憶體中的語義,即可見性和禁止重排優化。
下面看一 個非常典型的禁止重排優化的例子DCL,如下: 
      
public class DoubleCheckLock { 
    private static DoubleCheckLock instance; 
    private DoubleCheckLock(){} 
    public static DoubleCheckLock getInstance(){ //第一次檢測 
    if (instance==null){ //同步 synchronized (DoubleCheckLock.class) 
       { if (instance == null){ //多線程環境下可能會出現問題的地方 
              instance = new  DoubleCheckLock();
             }
          }
      }
    return instance;
  }
}      
上述代碼一個經典的單例的雙重檢測的代碼,這段代碼在單線程環境下并沒有什麼問題, 但如果在多線程環境下就可以出現線程安全問題。原因在于某一個線程執行到第一次檢測,讀 取到的instance不為null時,instance的引用對象可能沒有完成初始化。


總線風暴 
問題:大量使用cas和volatile,會有什麼問題? 高并發情況下為什麼會産生總線風暴?
1. CAS ---> CPU工作記憶體與主記憶體産生大量的互動
2. volatile ---> 産生大量的無效的工作記憶體變量

        
第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則

 解決辦法: 

synchronized 關鍵字


單例設計---高并發(加雙重鎖)
      
public class Singleton {

    /**
     * 檢視彙編指令
     * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
     */
    private volatile static Singleton myinstance;

    public static Singleton getInstance() {
        if (myinstance == null) {
            synchronized (Singleton.class) {
                if (myinstance == null) {
                    myinstance = new Singleton();//對象建立過程,本質可以分文三步
                    //對象延遲初始化
                    //
                }
            }
        }
        return myinstance;
    }

    public static void main(String[] args) {
        Singleton.getInstance();
    }
}      
第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則