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


CPU內部結構划分
控制單元
運算單元
存儲單元

 

計算機多硬件多CPU結構:

 

 
        

 

CPU緩存一致性原則

 

 

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三大特性
原子性
  匯編指令 --原子比較和交換在底層的支持 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". 

 

 

 

修改:

 

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

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

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

線程:threadB當前線程嗅探到initFlag的狀態的改變25747425

 

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

 

 


有序性
-- 指令重排 ---> 內存屏障(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;
    }
}
 
        
 
        

 

 


內存屏障 Memory Barrier
1.寫寫storestore 2.寫讀storeload  3.讀寫loadstore 4.讀讀loadload 



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 ---> 產生大量的無效的工作內存變量

 

 解決辦法: 

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();
    }
}
 
        

 

 
        

 

 




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM