Java併發編程之指令重排序

指令重排序

若是說內存可見性問題已經讓你抓狂了,那麼下邊的這個指令重排序的事兒估計就要罵娘了~這事兒還得從一段代碼提及:java

public class Reordering {

    private static boolean flag;
    private static int num;

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                while (!flag) {
                    Thread.yield();
                }

                System.out.println(num);
            }
        }, "t1");
        t1.start();
        num = 5;
        flag = true;
    }
}

須要注意到flag並非一個volatile變量,也就是說它存在內存可見性問題,可是即使如此,num = 5也是寫在flag = true的前邊的,等到t1線程檢測到了flag值的變化,num值的變化應該是早於flag值刷新到主內存的,因此線程t1最後的輸出結果確定是5!!!編程

no!no!no! 輸出的結果也多是0,也就是說flag = true可能先於num = 5執行,有沒有亮瞎你的狗眼~ 這些代碼最後都會變成機器能識別的二進制指令,咱們把這種指令不按書寫順序執行的狀況稱爲指令重排序。大多數現代處理器都會採用將指令亂序執行的方法,在條件容許的狀況下,直接運行當前有能力當即執行的後續指令,避開獲取下一條指令所需數據時形成的等待。經過亂序執行的技術,處理器能夠大大提升執行效率。安全

Within-Thread As-If-Serial Semantics

既然存在指令重排序這種現象,爲何咱們以前寫代碼歷來沒感受到呢?到了多線程這才發現問題?多線程

指令重排序不是隨便排,一個一萬行的程序直接把最後一行當成第一行就給執行那不就逆天了了麼,指令重排序是須要遵循代碼依賴狀況的。好比下邊幾行代碼:併發

int i = 0, b = 0;
i = i + 5;  //指令1
i = i*2;  //指令2
b = b + 3;  //指令3

對於上邊標註的3個指令來講,指令2是對指令1有依賴的,因此指令2不能被排到指令1以前執行。可是指令3指令1指令2都沒有關係,因此指令3能夠被排在指令1以前,或者指令1指令2中間或者指令2後邊執行均可以~ 這樣在單線程中執行這段代碼的時候,最終結果和沒有重排序的執行結果是同樣的,因此這種重排序有着Within-Thread As-If-Serial Semantics的含義,翻譯過來就是線程內表現爲串行的語義ide

可是這種指令重排序單線程中沒有任何問題的,可是在多線程中,就引起了咱們上邊在執行flag = true後,num的值仍然不能肯定是0仍是5性能

抑制重排序

在多線程併發編程的過程當中,執行重排序有時候會形成錯誤的後果,好比一個線程在main線程中調用setFlag(true)的前邊修改了某些程序配置項,而在t1線程裏須要用到這些配置項,因此會形成配置缺失的錯誤。可是java給咱們提供了一些抑制指令重排序的方式。優化

同步代碼抑制指令重排序spa

將須要抑制指令重排序的代碼放入同步代碼塊中:線程

public class Reordering {

    private static boolean flag;
    private static int num;

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                while (!getFlag()) {
                    Thread.yield();
                }

                System.out.println(num);
            }
        }, "t1");
        t1.start();
        num = 5;
        setFlag(true);
    }

    public synchronized static void setFlag(boolean flag) {
        Reordering.flag = flag;
    }

    public synchronized static boolean getFlag() {
        return flag;
    }
}

在獲取鎖的時候,它前邊的操做必須已經執行完成,不能和同步代碼塊重排序;在釋放鎖的時候,同步代碼塊中的代碼必須所有執行完成,不能和同步代碼塊後邊的代碼重排序。

圖片描述

加了鎖以後,num=5就不能和flag=true的代碼進行重排序了,因此在線程2中看到的num值確定是5,而不會是0嘍~

雖然抑制重排序能夠保證多線程程序按照咱們指望的執行順序進行執行,可是它抑制了處理器對指令執行的優化,原來能並行執行的指令如今只能串行執行,會致使必定程度的性能降低,因此加鎖只能保證在執行同步代碼塊時,它以前的代碼已經執行完成,在同步代碼塊執行完成以前,代碼塊後邊的代碼是不能執行的,也就是隻保證加鎖前、加鎖中、加鎖後這三部分的執行時序,可是同步代碼塊以前的代碼能夠重排序,同步代碼塊中的代碼能夠重排序,同步代碼塊以後的代碼也能夠進行重排序,在保證執行順序的基礎上,盡最大可能讓性能獲得提高,比方說下邊這段代碼:

int i = 1;
int j = 2;
synchronized (Reordering.class) {
    int m = 3;
    int n = 4;
}
int x = 5;
int y = 6;

它的一個執行時序多是:

圖片描述

volatile變量抑制指令重排序

仍是那句老話,加鎖會致使競爭同一個鎖的線程阻塞,形成線程切換,代價比較大,volatile變量也提供了一些抑制指令重排序的語義,上邊的程序能夠改爲這樣:

public class Reordering {

    private static volatile boolean flag;
    private static int num;

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                while (!flag) {
                    Thread.yield();
                }

                System.out.println(num);
            }
        });
        t1.start();
        num = 5;
        flag = true;
    }
}
``
也就是把``flag``聲明爲``volatile變量``,這樣也能起到抑制重排序的效果,``volatile變量``具體抑制重排序的規則以下:

1. volatile寫以前的操做不會被重排序到volatile寫以後。
2. volatile讀以後的操做不會被重排序到volatile讀以前。
3. 前邊是volatile寫,後邊是volatile讀,這兩個操做不能重排序。
![圖片描述][3]
除了這三條規定之外,其餘的操做能夠由處理器按照本身的特性進行重排序,換句話說,就是怎麼執行着快,就怎麼來。好比說:

flag = true;
num = 5;
``
volatile變量以後進行普通變量的寫操做,那就能夠重排序嘍,直到遇到一條volatile讀或者有執行依賴的代碼纔會阻止重排序的過程。

final變量抑制指令重排序

在java語言中,用final修飾的字段被賦予了一些特殊的語義,它能夠阻止某些重排序,具體的規則就這兩條:

  1. 在構造方法內對一個final字段的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。
  2. 初次讀一個包含final字段對象的引用,與隨後初次讀這個final字段,這兩個操做不能重排序。

可能你們看的有些懵逼,趕忙寫代碼理解一下:

public class FinalReordering {

    int i;
    final int j;

    static FinalReordering obj;

    public FinalReordering() {
        i = 1;
        j = 2;
    }

    public static void write() {
        obj = new FinalReordering();
    }

    public static void read() {
        FinalReordering finalReordering = FinalReordering.obj;
        int a = finalReordering.i;
        int b = finalReordering.j;
    }
}

咱們假設有一個線程執行write方法,另外一個線程執行read方法。

先看一下對final字段進行寫操做時,不一樣線程執行write方法和read方法的一種可能狀況是:

圖片描述

從上圖中能夠看出,普通的字段可能在構造方法完成以後才被真正的寫入值,因此另外一個線程在訪問這個普通變量的時候可能讀到了0,這顯然是不符合咱們的預期的。可是final字段的賦值不容許被重排序到構造方法完成以後,因此在把該字段所在對象的引用賦值出去以前,final字段確定是被賦值過了,也就是說這兩個操做不能被重排序

再來看一下初次讀取final字段的狀況,下邊是不一樣線程執行write方法和read方法的一種可能狀況:
圖片描述

從上圖能夠看出,普通字段的讀取操做可能被重排序到讀取該字段所在對象引用前邊,天然會獲得NullPointerException異常嘍,可是對於final字段,在讀final字段以前,必須保證它前邊的讀操做都執行完成,也就是說必須先進行該字段所在對象的引用的讀取,再讀取該字段,也就是說這兩個操做不能進行重排序

值得注意的是,讀取對象引用與讀取該對象的字段是存在間接依賴的關係的,對象引用都沒有被賦值,還讀個錘子對象的字段嘍,通常的處理器默認是不會重排序這兩個操做的,但是有一些爲了性能不顧一切的處理器,好比alpha處理器,這種處理器是可能把這兩個操做進行重排序的,因此這個規則就是給這種處理器貼身設計的~ 也就是說對於final字段,無論在什麼處理器上,都得先進行對象引用的讀取,再進行final字段的讀取。可是並不保證在全部處理器上,對於對象引用讀取和普通字段讀取的順序是有序的。

安全性小結

咱們上邊介紹了原子性操做內存可見性以及指令重排序三個在多線程執行過程當中會影響到安全性的問題。

  • synchronized能夠把三個問題都解決掉,可是伴隨着這種萬能特性,是多線程在競爭同一個鎖的時候會形成線程切換,致使線程阻塞,這個對性能的影響是很是大的。
  • volatile不能保證一系列操做的原子性,可是能夠保證對於一個變量的讀取和寫入是原子性的,一個線程對某個volatile變量的寫入是能夠當即對其餘線程可見的,另外,它還能夠禁止處理器對一些指令執行的重排序。
  • final變量依靠它的禁止重排序規則,保證在使用過程當中的安全性。一旦被賦值成功,它的值在以後程序執行過程當中都不會改變,也不存在所謂的內存可見性問題。
相關文章
相關標籤/搜索