Jvm 中的 重排序、主存、原子操做

1、重排序

好處:重排序能夠提高性能,避免在一個耗時很長的指令在「執行」階段呆很長時間,而致使後續的指令都卡在「執行」以前的階段上。java

壞處:重排序對多線程的影響c++

class ReorderExample {
    int a = 0;
    boolean flag = false;

    public void writer() {
        a = 1;                   //1
        flag = true;             //2
    }

    Public void reader() {
        if (flag) {              //3
            int i =  a * a;      //4
            ……
        }
    }
}

 

1) 數據依賴性:因此有數據依賴性的語句不能進行重排序。程序員

 2) as-if-serial:若是操做之間不存在數據依賴關係,這些操做可能被編譯器和處理器重排序。可是無論怎麼重排序,必須保證單線程程序的執行結果不能被改變,故單線程中重排序不須要被禁止(不影響執行結果)。緩存

double pi  = 3.14;        // Ⓐ
double r   = 1.0;         // Ⓑ  
double area = pi * r * r; // Ⓒ

Ⓐ -> Ⓑ -> Ⓒ 按程序順序的執行結果:area = 3.14多線程

Ⓑ -> Ⓐ -> Ⓒ 按重排序後的執行結果:area = 3.14app

注:as-if-serial 語義把單線程程序保護了起來,遵照as-if-serial語義的編譯器,寫單線程的程序員有一個幻覺:單線程程序是按程序寫的順序來執行的。性能

 3) happens-before 規則線程

  • 程序順序規則:按照程序代碼的執行流順序,(時間上)先執行的操做happen—before(時間上)後執行的操做。
  • 監視器鎖規則:對一個監視器鎖的解鎖,happens- before 於隨後對這個監視器鎖的加鎖。
  • volatile變量規則:對一個volatile域的寫,happens- before 於任意後續對這個volatile域的讀。
  • 傳遞性:若是A happens- before B,且B happens- before C,那麼A happens- before C。

 

 

2、主存與可見性

1. 主存與工做內存blog

計算機系統中,爲了儘量地避免處理器訪問主內存的時間開銷,處理器大多會利用緩存(cache)以提升性能。其模型以下圖所示:排序

   在這種模型下會存在一個現象,即緩存中的數據與主內存的數據並非實時同步的,各CPU(或CPU核心)間緩存的數據也不是實時同步的。這致使在同一個時間點,各CPU所看到同一內存地址的數據的值多是不一致的。從程序的視角來看,就是在同一個時間點,各個線程所看到的共享變量的值多是不一致的。 

   有的觀點會將這種現象也視爲重排序的一種,命名爲「內存系統重排序」。由於這種內存可見性問題形成的結果就好像是內存訪問指令發生了重排序同樣(實際上內存指令沒有重排序,僅僅是內存可見性問題致使的結果)。

  Java內存模型規定全部的變量都是存在主存當中,每一個線程都有本身獨立的工做內存,裏面保存該線程的使用到的變量副本(該副本就是主內存中該變量的一份拷貝 )。線程對變量的全部操做都必須在工做內存中進行,而不能直接對主存進行操做。而且每一個線程不能訪問其餘線程的工做內存。

 

2. 可見性

內存可見性其針對的是 共享資源在工做內存與主存之間的相互訪問。

保證可見性的原義:指的是工做內存中對共享資源修改當即刷入到主存中,而且主存中的值當即同步到其它工做內存中。

它的實際實現方式能夠是:

1) 使用 synchronized,線程在加鎖時,先清空工做內存→在主內存中拷貝最新變量的副本到工做內存→執行完代碼→將更改後的共享變量的值刷新到主內存中→釋放互斥鎖。

2) 使用 volatile,也就是對volatile變量執行寫操做時,會在寫操做後加入一條store指令,即強迫線程將最新的值刷新到主內存中,而其餘工做內存中的值失效;而在讀操做時,會加入一條load指令,即強迫從主內存(工做內存中的值已經失效)中讀入變量的值。volatile 防止重排序針對的是內存屏障。但volatile不保證volatile變量的原子性。

 

 

3、原子操做

原子操做是指整個操做過程不會被線程調度機制打斷。

使用 voilate 關鍵字 既可避免重排序問題,又可避免內存可見性問題,但沒法解決非原子性問題。好比自增操做就不是原子性操做:

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的數字。

緣由在於,自增操做是不具有原子性的,它包括讀取變量的原始值、進行加1操做、寫入工做內存。那麼就是說自增操做的三個子操做可能會分割開執行,就有可能致使下面這種狀況出現:

      a) 線程1對變量進行自增操做:線程1先讀取變量inc的原始值,而後線程1被阻塞了(尚未 inc 的值);

       b) 而後線程2對變量進行自增操做:線程2也去讀取變量inc的原始值,因爲線程1只是對變量inc進行讀取操做,而沒有對變量進行修改操做,因此不會致使線程2的工做內存中緩存變量inc的緩  存行無效,因此線程2會直接去主存讀取inc的值,發現inc的值時10,而後進行加1操做,並把11寫入工做內存,最後寫入主存。

       c) 而後線程1接着進行加1操做,因爲已經讀取了inc的值,此時線程1的工做內存中inc的值仍然爲10,因此線程1對inc進行加1操做後inc的值爲11,而後將11寫入工做內存,最後寫入主存。

       d) 那麼兩個線程分別進行了一次自增操做後,inc只增長了1。

相關文章
相關標籤/搜索