併發編程之Java內存模型

一。共享內存模型數組

    共享內存模型指的就是Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入時,能對另外一個線程可見。緩存

    從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:安全

    線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。多線程

    本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。app

    當多個線程同時訪問一個數據的時候,可能本地內存沒有及時刷新到主內存,因此就會發生線程安全問題。ide

二。Volatile性能

    一旦某個線程修改了被volatile修飾的基本類型變量,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,能夠當即獲取修改以後的值。優化

    在Java中爲了加快程序的運行效率,對一些變量的操做一般是在該線程的寄存器或是CPU緩存上(即本地內存)進行的,以後纔會同步到主存中,而加了volatile修飾符的變量則是直this

    接讀寫主存。Volatile 保證了線程間共享變量的及時可見性,但不能保證原子性。spa

class ThreadVolatileDemo extends Thread {
    public boolean flag = true;
    @Override
    public void run() {
        System.out.println("開始執行子線程....");
        while (flag) {
        }
        System.out.println("線程中止");
    }
    public void setRuning(boolean flag) {
        this.flag = flag;
    }
}

public class ThreadVolatile {
    public static void main(String[] args) throws InterruptedException {
        ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
        threadVolatileDemo.start();
        Thread.sleep(3000);
        threadVolatileDemo.setRuning(false);
        System.out.println("flag 已經設置成false");
        Thread.sleep(1000);
        System.out.println(threadVolatileDemo.flag);
    }
}

能夠看到結果爲:flag已經設置爲false了,但子線程卻還在運行,問題就出在線程之間是不可見的,讀取的是副本,沒有及時讀取到主內存結果。

解決辦法使用Volatile關鍵字將解決線程之間可見性, 強制線程每次讀取該值的時候都去「主內存」中取值。

    1。Volatile特性:

        1>   保證基本類型變量的可見性,即一個線程修改了某個基本類型變量的值,這新值對其餘線程來講是當即可見的。

               但對於引用類型如數組,實體bean,僅僅保證引用的可見性,但並不保證引用內容的可見性,須要基於CAS的原子結構才行(CAS的原子結構在鎖的深刻會講到)。

        2>   禁止進行指令重排序。(重排序後面會講到)

    2。volatile 性能:

        volatile 的讀性能消耗與普通變量幾乎相同,可是寫操做稍慢,由於它須要在本地代碼中插入許多內存屏障指令來保證處理器不發生重排序優化。

    3。Volatile與Synchronized區別:

        1>   volatile僅能使用在變量級別。synchronized則可使用在變量、方法、和類級別的。

        2>  volatile僅能實現變量的修改可見性,並不能保證原子性.。synchronized則能夠保證變量的修改可見性和原子性。

        3>  volatile不會形成線程的阻塞。synchronized會形成線程的阻塞。

        4>  volatile標記的變量不會被編譯器優化。synchronized標記的變量能夠被編譯器優化。

        5>  volatile修飾的變量在兩個線程並行時,一個線程修改其值後會強制改變另一個線程的該變量值。

             例如多線程狀況下須要用到某變量共享數據,當進行只讀操做時可使用volatile修飾變量。

             當進行寫操做的時候,因爲volatile沒法保證原子性因此建議使用synchronized來修飾變量。

三。重排序

    在執行程序時,編譯器和處理器會對指令進行重排序,重排序分爲:

        1>  編譯器重排序:在不改變代碼語義的狀況下,優化性能而改變了代碼執行順序。

        2>  指令並行的重排序:處理器採用並行技術使多條指令重疊執行,在不存在數據依賴的狀況下,改變機器指令的執行順序。

        3>  內存系統的重排序:使用緩存和讀寫緩衝區時,加載和存儲多是亂序執行。

    好比編譯器重排序的典型就是經過調整指令順序,在不改變程序語義的前提下,儘量的減小寄存器的讀取、存儲次數,充分複用寄存器的存儲值。

    int a = 5;①            int b = 10;②              int c = a + 1;③          假設用的同一個寄存器

    這三條語句,若是按照順序一致性,執行順序爲①②③寄存器要被讀寫三次;但爲了下降重複讀寫的開銷,編譯器會交換第二和第三的位置,即執行順序爲①③②。

    再好比對於不存在數據依賴的操做,前面new操做比較費時間,但也會先執行後面省時間的操做。

    1。數據依賴性

    若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性。數據依賴分下列三種類型:

名稱

代碼示例

說明

寫後讀

a = 1;b = a;

寫一個變量以後,再讀這個位置。

寫後寫

a = 1;a = 2;

寫一個變量以後,再寫這個變量。

讀後寫

a = b;b = 1;

讀一個變量以後,再寫這個變量。

    上面三種狀況,只要重排序兩個操做的執行順序,程序的執行結果將會被改變。

    編譯器和處理器在重排序時,會遵照數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序。

    這裏所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操做,不一樣處理器之間和不一樣線程之間的數據依賴性不被編譯器和處理器考慮。

    2。as-if-serial語義

    無論怎麼重排序(編譯器和處理器爲了提升並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵照as-if-serial語義。

    爲了遵照as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。

    可是,若是操做之間不存在數據依賴關係,這些操做可能被編譯器和處理器重排序。好比

double pi  = 3.14;    //A

double r   = 1.0;     //B

double area = pi * r * r; //C

    上面三個操做的數據依賴關係以下圖所示:

    如上圖所示,A和C之間存在數據依賴關係,同時B和C之間也存在數據依賴關係。

    所以在最終執行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結果將會被改變)。

    但A和B之間沒有數據依賴關係,編譯器和處理器能夠重排序A和B之間的執行順序。

    3。程序次序規則(Program Order Rule)

    根據happens- before的程序順序規則,上面計算圓的面積的示例代碼存在三個happens- before關係:

        1>    A happens- before B;
        2>    B happens- before C;
        3>    A happens- before C;

    這裏A happens- before B,但實際執行時B卻能夠排在A以前執行,在Java中,若是A happens- before B,JMM並不要求A必定要在B以前執行。

    JMM僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前。這裏操做A的執行結果不須要對操做B可見;

    並且重排序操做A和操做B後的執行結果,與操做A和操做B按happens- before順序執行的結果一致。

    在這種狀況下,JMM會認爲這種重排序並不非法(not illegal),JMM容許這種重排序。

    在計算機中,軟件技術和硬件技術有一個共同的目標:在不改變程序執行結果的前提下,儘量的開發並行度。

    編譯器和處理器聽從這一目標,從happens- before的定義咱們能夠看出,JMM一樣聽從這一目標。

    4。重排序對多線程的影響

    重排序可能會改變多線程程序的執行結果。

public class SimpleHappenBefore {
    private static int a=0;
    private static boolean flag=false;

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<1000;i++){
            Thread t1 = new Thread(new Runnable() {
                public void run() {
                    a=1;
                    flag=true;
                }
            });
            Thread t2 = new Thread(new Runnable() {
                public void run() {
                    if(flag){
                        a=a*1;
                    }
                    if(a==0){
                        System.out.println("ha,a==0");
                    }
                }
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();
            a=0;
            flag=false;
        }
    }
}

     若是按照有序的話,那麼在ThreadB中若是if(flag)成功的話,則應該a=1,而a=a*1以後a仍然爲1,下方的if(a==0)應該永遠不會爲真,永遠不會打印。而由於重排序卻打印了幾十次

相關文章
相關標籤/搜索