【漫畫】JAVA併發編程 如何解決可見性和有序性問題

原創聲明:本文來自公衆號【胖滾豬學編程】,以漫畫形式讓編程so easy and interesting,轉載請註明出處!java

在上一篇文章併發編程三大源頭中,咱們初識了併發編程的三個bug源頭:可見性、原子性、有序性。明白了它們究竟爲何會發生,那麼今天咱們就來聊聊如何解決這三個問題吧。面試

序幕

_1

Happens-Before是什麼?

_2

A Happens-Before B 意味着 A 事件對 B 事件來講是可見的,不管 A 事件和 B 事件是否發生在同一個線程裏。例如 A 事件發生在線程 1 上,B 事件發生在線程 2 上,Happens-Before 規則保證線程 2 上也能看到 A 事件的發生。編程

_3

Happens-Before的做用

原創聲明:本文來自公衆號【胖滾豬學編程】,以漫畫形式讓編程so easy and interesting,轉載請註明出處!緩存

happens-before原則很是重要,它是判斷線程是否安全的主要依據,依靠這個原則,咱們就能解決在併發環境下可見性和有序性問題。 好比某天老闆問你「胖滾豬,我這段併發代碼會有線程安全問題嗎」,那麼你能夠對照着happens-before原則一個個看,要是符合其中之一而且是原子性的,你就能夠大聲告訴老闆「沒得問題!」 好比這段代碼:安全

i = 1;       //線程A執行
j = i ;      //線程B執行複製代碼

j 是否等於1呢?假定線程A的操做(i = 1)happens-before線程B的操做(j = i),那麼能夠肯定線程B執行後j = 1 必定成立,若是他們不存在happens-before原則,那麼j = 1 不必定成立。 這就是happens-before原則的威力!讓咱們走進它的世界吧!多線程

Happens-Before八大原則 解決原子性和有序性問題

image.png

###規則一:程序的順序性規則 這條規則是指在一個線程中,按照程序順序,前面的操做 Happens-Before 於後續的任意操做。這規則挺好理解的,畢竟是在一個線程中吶。 你會以爲這是個廢物規則。其實這個規則是一個基礎規則,happens-before 是多線程的規則,因此要和其餘規則約束在一塊兒才能體現出它的順序性,彆着急,繼續向下看。併發

###規則二: Volatile變量規則 這條規則是指對一個 volatile 變量的寫操做, Happens-Before 於後續對這個 volatile 變量的讀操做。咱們在上篇文章說過,由於緩存的緣由,每一個線程有本身的工做內存,若是共享變量沒有及時刷到主內存中,那就會致使可見性問題,線程B沒有及時讀到線程A的寫。可是隻要加上Volatile,就能夠避免這個問題,至關於volatile的做用是對變量的修改會繞太高速緩存馬上刷新到主存。不過要注意一下,volatile除了保證可用性,它還能夠禁止指定重排序哦!app

public class TestVolatile1 {
    private volatile static int count = 0;
    public static void main(String[] args) throws Exception {
        final TestVolatile1 test = new TestVolatile1();
        Thread th1 = new Thread(() -> {
            count = 10;
        });
        Thread th2 = new Thread(() -> {
            //沒有volatile修飾count的話極小機率會出現等於0的狀況
            System.out.println("count=" + count);
        });
        // 啓動兩個線程
        th1.start();
        th2.start();
    }
}複製代碼

規則三: 傳遞性規則

這條規則是指若是 A Happens-Before B,且 B Happens-Before C,那麼 A Happens-Before C。這也很好理解。咱們舉個例子,writer和reader是兩個不一樣的線程,它們有以下操做:ide

int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42; //(1)
    v = true; //(2)
  }
  public void reader() {
    if (v == true) { //(3)
      // 這裏 x 會是多少呢?(4)
    }
  }複製代碼

這個例子和上面那個Volatile的例子有個區別就是,有兩個變量。那麼咱們來分析一下: (1)和(2)在同一個線程中,根據規則1,(1)Happens-Before於(2) (3)和(4)在同一個線程中,同理,(3)Happens-Before於(4) 根據規則2,因爲v用了volatile修飾,那麼(2)必然 Happens-Before於(3)。 那麼根據傳遞性規則可得:(1)Happens-Before於(4),所以x必然爲42。 因此即便x沒有用volatile,它也是能夠保證可見性的!因此爲啥剛剛說規則1要和其餘規則聯合起來看纔有意思,如今你知道了吧!函數

規則四: 管程中的鎖規則

指管程中的解鎖必然發生在隨後的加鎖以前。管程是一種通用的同步原語,synchronized 是 Java 裏對管程的實現。管程中的鎖在 Java 裏是隱式實現的,例以下面的代碼,在進入同步塊以前,會自動加鎖,而在代碼塊執行完會自動釋放鎖,加鎖以及釋放鎖都是編譯器幫咱們實現的。

synchronized (this) { // 此處自動加鎖
  if (this.x < 10) {//臨界區
  }  
} // 此處自動解鎖複製代碼

這個規則比較好理解,不管是在單線程環境仍是多線程環境,一個鎖處於被鎖定狀態,那麼必須先執行unlock操做後面才能進行lock操做。 _4

規則五: 線程啓動規則

主線程 A 啓動子線程 B 後(線程 A 調用線程 B 的 start() 方法),子線程 B 可以看到主線程在啓動子線程 B 前的操做。

private static long count = 0;
public static void main(String[] args) throws InterruptedException {
    Thread B = new Thread(() -> {
        // 主線程調用 B.start() 以前 全部對共享變量的修改,此處皆可見
        // 所以count確定爲10
        System.out.println(count);
    });
    // 此處對共享變量count修改
    count = 10;
    // 主線程啓動子線程
    B.start();
}複製代碼

規則六: 線程終止規則

主線程 A 等待子線程 B 完成(主線程 A 經過調用子線程 B 的 join() 方法實現),若是在線程 A 中,調用線程 B 的 join() 併成功返回,那麼主線程可以看到子線程的操做(指共享變量的操做),換句話說就是線程 B 中的任意操做 Happens-Before 於該 join() 操做的返回。

private static long count = 0;
public static void main(String[] args) throws InterruptedException {
    Thread B = new Thread(() -> {
        // 主線程調用 B.start() 以前 全部對共享變量的修改,此處皆可見
        // 所以count確定爲10
        count = 10;
    });

    // 主線程啓動子線程
    B.start();
    // 主線程等待子線程完成
    B.join();
    // 子線程全部對共享變量的修改 在主線程調用 B.join() 以後皆可見
    System.out.println(count);//count必然爲10
}複製代碼

規則七:線程中斷規則

對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。即線程A調用線程B的interrupt()方法,happens-before於線程A發現B被A中斷(經過Thread.interrupted()方法檢測到是否有中斷髮生)。

private static long acount = 0;
private static long bcount = 0;
public static void main(String[] args) throws InterruptedException {
    Thread B = new Thread(() -> {
        bcount = 7;
        System.out.println("Thread A被中斷前bcount="+bcount+" acount="+acount);
        while (true){
            if (Thread.currentThread().isInterrupted()){
                bcount = 77;
                System.out.println("Thread A被中斷後bcount="+bcount+" acount="+acount);
                return;
            }
        }
    });
    B.start();
    Thread A = new Thread(() -> {
        acount = 10;
        System.out.println("Thread B 中斷A前bcount="+bcount+" acount="+acount);
        B.interrupt();
        acount = 100;
        System.out.println("Thread B 中斷A後bcount="+bcount+" acount="+acount);
    });
    A.start();
}複製代碼

規則八:對象規則

一個對象的初始化完成(構造函數執行結束,通常都是用new初始化)happen—before它的finalize()方法的開始。finalize()是在java.lang.Object裏定義的,即每個對象都有這麼個方法。這個方法在該對象被回收的時候被調用。該條原則強調的是多線程狀況下對象初始化的結果必須對發生於其後的對象銷燬方法可見。

public HappensBefore8(){
        System.out.println("構造方法");
    }
    @Override
    protected void finalize() throws Throwable {
        System.out.println("對象銷燬");
    }

    public static void main(String[] args){
        new HappensBefore8();
        System.gc();
    }複製代碼

關於有序性的那些疑問

原創聲明:本文來自公衆號【胖滾豬學編程】,以漫畫形式讓編程so easy and interesting,轉載請註明出處! _5

擴展有序性的概念:Java內存模型中的程序自然有序性能夠總結爲一句話,若是在本線程內觀察,全部操做都是有序的;若是在一個線程中觀察另外一個線程,全部操做都是無序的。前半句是指「線程內表現爲串行語義」,後半句是指「指令重排序」現象和「工做內存主主內存同步延遲」現象。 這其實還涉及到一個高頻面試考點:as-if-serial語義

as-if-serial語義:無論怎麼重排序,單線程程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵照as-if-serial語義。因此編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。可是,若是操做之間不存在數據依賴關係,這些操做就可能被編譯器和處理器重排序。

劃重點:單線程中保證按照順序執行。 synchronized同一時刻只有一個線程在運行,也就至關於保證了有序性。至於這個雙重檢查案例,出問題,並非由於synchronized沒有保證有序性。而是指令重排致使了在多個線程中無序。

總結

_6

原創聲明:本文來自公衆號【胖滾豬學編程】,以漫畫形式讓編程so easy and interesting,轉載請註明出處! 本文轉載自公衆號【胖滾豬學編程】 用漫畫讓編程so easy and interesting!歡迎關注!

相關文章
相關標籤/搜索