深刻淺出併發編程底層原理

1.Java內存模型——底層原理

1.1 什麼是底層原理

Java程序編譯到運行須要通過將.java後綴的文件經過javac命令編譯成.class文件(此時與平臺無關),而後將對應的.class文件轉化成機器碼並執行,可是因爲不一樣平臺的JVM會帶來不一樣的「翻譯」,因此咱們在Java層寫的各類Lock,其實最終依賴的是JVM的具體實現和CPU指令,才能幫助咱們達到線程安全的效果。java

2 三兄弟:JVM內存結構、Java內存模型、Java對象模型

2.1 JVM內存結構,和Java虛擬機的運行時數據區有關

  • 堆:堆是內存結構中最大的一塊區域,線程共享而且動態分配內存,當建立了一個對象就會在堆上分配內存,當堆滿了以後會觸發GC進行垃圾回收。
  • 方法區:方法區是線程共享的,方法區用於存儲類信息、常量以及靜態變量。
  • Java棧(虛擬機棧):虛擬機棧是線程私有的,它的內存是不可變的,也就是說在編譯時就已經肯定了的,虛擬機棧用於存儲局部變量表、操做數棧、動態連接和方法出口。
  • 本地方法棧:本地方法棧的做用於虛擬機棧相似,區別在於一個服務於Java方法一個服務於native方法
  • 程序計數器:程序計數器是線程私有的,它佔用的空間很是小也是惟一一個不存在OOM問題的區域,主要用於記錄程序執行的行號數。

2.2 Java內存模型,和Java的併發編程有關

下面介紹面試

2.3 Java對象模型,和Java對象在虛擬機中的表現形式有關

Java對象模型是Java 對象自身的 存儲模型,JVM會給一個類建立一個instanceKlass,保存在 方法區中,用來在JVM層表示該Java類。

在使用new指令建立一個對象的時候,JVM會建立一個instanceOopDesc對象,這個對象中包含了對象頭以及實例數據編程

3.JMM(Java Memory Model)

3.1 爲何須要JMM

C/C++語言它們不存在內存模型的概念,它們依賴於處理器,不一樣的處理器處理的結果不一樣,也就沒法保證併發安全。因此此時須要一個標準,讓多線程的運行結果可預期緩存

3.2 什麼是JMM

JMM是一組規範,要求JVM依照規範來實現,從而讓咱們更好的開發多線程程序。若是沒有了JMM規範,那麼不一樣的虛擬機可能會進行不一樣的重排序,這樣就會致使不一樣的虛擬機上運行的結果不一樣,這也就引法了問題。安全

JMM除了是規範仍是工具類和關鍵字的原理,咱們常見的volatilesynchronized以及Lock等的原理都是JMM。若是沒有JMM,那就須要咱們本身指定何時須要內存柵欄(工做內存與主內存之間的拷貝同步)等,這樣就很麻煩,由於有了JMM,因此咱們只須要使用關鍵字就能夠開發併發程序了。bash

4.JMM之重排序

第一種執行狀況多線程

/**
 * 演示重排序的現象
 * 「直到達到某個條件才中止」,測試小几率事件
 */
public class OutOfOrderExecution {

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread one = new Thread(new Runnable() {
            @Override
            public void run() {
                a = 1;
                x = b;
            }
        });
        Thread two = new Thread(new Runnable() {
            @Override
            public void run() {
                b = 1;
                y = a;
            }
        });
        one.start();
        two.start();
        one.join();
        two.join();
        System.out.println("x = " + x + ", y = " + y);
    }
}
複製代碼

第二種執行狀況併發

/**
 * 演示重排序的現象
 * 「直到達到某個條件才中止」,測試小几率事件
 */
public class OutOfOrderExecution {

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread one = new Thread(new Runnable() {
            @Override
            public void run() {
                a = 1;
                x = b;
            }
        });
        Thread two = new Thread(new Runnable() {
            @Override
            public void run() {
                b = 1;
                y = a;
            }
        });
        two.start();
        one.start();
        one.join();
        two.join();
        System.out.println("x = " + x + ", y = " + y);
    }
}
複製代碼

第三種執行狀況

/**
 * 演示重排序的現象
 * 「直到達到某個條件才中止」,測試小几率事件
 */
public class OutOfOrderExecution {

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
    
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        Thread one = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();  //進行等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                a = 1;
                x = b;
            }
        });
        Thread two = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();  //進行等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                b = 1;
                y = a;
            }
        });
        one.start();
        two.start();
        latch.countDown();  //統一開始執行
        one.join();
        two.join();
        System.out.println("x = " + x + ", y = " + y);
    }
}
複製代碼

對第三種狀況的優化

/**
 * 演示重排序的現象
 * 「直到達到某個條件才中止」,測試小几率事件
 */
public class OutOfOrderExecution {

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;  //計數
        for (; ; ) {
            i++;
            x = 0;  //清零操做
            y = 0;
            a = 0;
            b = 0;

            CountDownLatch latch = new CountDownLatch(1);
            Thread one = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a = 1;
                    x = b;
                }
            });
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    b = 1;
                    y = a;
                }
            });
            one.start();
            two.start();
            latch.countDown();
            one.join();
            two.join();
            String result = "第" + i + "次 (" + x + ", " + y + ")";
            if (x == 1 && y == 1) {
                System.out.println(result);
                return; //知足條件後退出循環
            } else {
                System.out.println(result);
            }
        }
    }
}
複製代碼

總結:代碼的執行順序決定了執行的結果

4.1 眼見爲實的重排序

只需將上面的結束條件改成x == 0 && y == 0便可 app

  • 爲何會出現x=0, y=0 ?

出現這種狀況是由於重排序發生了,代碼的執行順序有可能爲ide

y = a;
a = 1;
x = b;
b = 1;
複製代碼
  • 什麼是重排序

線程1中代碼的執行順序與Java代碼不一致,代碼的執行順序並非按照指令執行的,它們的執行順序被改變了,這就是重排序

4.2 重排序的好處與發生的時機

對比下圖能夠發現若是進行重排序能夠減小關於變量a的執行指令,若是在程序中個存在大量的相似狀況,也就提升了處理速度

4.3 重排序的3種狀況

  • 編譯器優化:包括JVM,JIT編譯器等

好比存在變量a和b,若是將對a的操做連續執行效率更高的話,就可能發生重排序來提升執行效率。

  • CPU指令重排:就算編譯器不發生重排,CPU也可能對指令進行重排

CPU重排和編譯器重排相似,就算編譯器不重排CPU也會進行重排,它們都是打亂執行順序達到優化的目的。

  • 內存的「重排序」:線程A的修改線程B卻看不到,引出可見性問題

內存中的重排序並不是真正的重排序,由於內存中有緩存的存在,在JMM中表現爲本地內存和主內存,若是線程1修改了變量a的值尚未來得及寫入到主存,此時線程2因爲可見性的緣由沒法知道線程1對變量進行了修改,因此會使程序表現出亂序行爲。

5.JMM之可見性

5.1 什麼是可見性

當一個線程執行寫操做時,另一個線程沒法看見此時被更改的值。就像下圖所示當線程1從主存中讀取變量x,並將x的值設置爲1,可是此時線程1並無將x的值寫回主存,因此線程2就沒法得知x的值已經改變了。

演示代碼

/**
 * 演示可見性帶來的問題
 */
public class FieldVisibility {

    int a = 1;
    int b = 2;

    public static void main(String[] args) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
    }

    private void print() {
        System.out.println("b = " + b + "; a = " + a);
    }

    private void change() {
        a = 3;
        b = a;
    }
}
複製代碼

四種狀況

a = 3; b = 3;
a = 1; b = 2;
a = 3; b = 2;
a = 1; b = 3;   //發生可見性問題
複製代碼

5.2 解決可見性問題——使用volatile

/**
 * 演示可見性帶來的問題
 */
public class FieldVisibility {
    
    //解決可見性問題
    volatile int a = 1;
    volatile int b = 2;

    public static void main(String[] args) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
    }

    private void print() {
        System.out.println("b = " + b + "; a = " + a);
    }

    private void change() {
        a = 3;
        b = a;
    }
}
複製代碼

volatile怎麼解決可見性問題

當線程1讀取到x並將值更新爲1後會刷回主存,當線程2再次讀取x時就會從主存中加載,這樣就不會引起可見性的問題。

5.3 爲何會出現可見性問題

引起可見性問題的緣由是由於讀取數據時須要一層一層對數據進行緩存,若是直接從RAM中讀取數據的話,這樣會大量下降讀取速度,這也是須要JMM的緣由。

5.4 JMM的抽象——主內存和本地內存

Java做爲高級語言,屏蔽了這些底層細節,用JMM定義了這些讀寫內存數據的規範,雖然咱們再也不須要關心一級緩存和二級緩存的問題,可是JMM抽象了主內存和本地內存的概念。

這裏說的本地內存並非真正的爲每一個線程分配一塊內存,而是JMM的抽象,是對寄存器、一級緩存、二級緩存的抽象

主內存和本地內存的關係

JMM有如下規定

  • 全部的變量都存儲在內存中,每一個線程中有獨立的工做內存,工做內存中的變量是主內存中的拷貝
  • 線程不能直接操做主內存中的變量,只能經過本身的工做內存讀取主內存中的變量再寫回去。
  • 主內存是線程共享的,可是工做內存不是,若是線程之間通訊必須經過主內存進行中轉

總結:線程操做數據必須從主內存中讀取數據,而後在本身的工做內存中進行操做,操做完成後再寫回主內存,由於讀寫須要時間因此就會引起可見性的問題

6.Happens-Before規則有哪些?

  • 單線程原則

在單線程狀況下,後面的語句必定能看到前面的語句作了什麼

  • 鎖操做(synchronized和Lock)

加鎖以後能看到解鎖以前的所有操做

  • volatile變量

volatile修飾的變量只要執行了寫操做,就必定會被讀取到

  • 線程啓動

調用start()方法可讓子線程中全部語句看到啓動以前的結果

  • 線程join

join() 後的語句能看到等待以前的全部操做

  • 傳遞性

好比第一行代碼運行後第二行會看到,第二行運行後第三行會看到,從中能夠推斷出第一行代碼運行完第三行就會看到。

  • 中斷

若是一個線程被interrupt()時,那麼isInterrupt()或者InterruptException必定能看到。

  • 構造方法

對象構造方法的最後一行語句happens-before於finalize()的第一行語句

  • 工具類的Happens-Before原則
    • 線程安全的容器get必定能看到在此以前的put操做
    • CountDownLatch
    • CyclicBarrier
    • Semaphore
    • Future
    • 線程池

7.volatile關鍵字

7.1 什麼是volatile

volatile是一種同步機制,相對synchronizedLock更輕量,不會帶來上下文切換等重大開銷。若是一個變量被volatile修飾,那麼JVM就知道這個變量可能會被併發修改。雖然volatile的開銷小,可是它的能力也小,相對於synchronized來講volatile沒法保證原子性

7.2 volatile的使用場景

  • 不適用於組合操做
/**
 * 不適用volatile的場景
 */
public class NoVolatile implements Runnable {

    volatile int a;
    AtomicInteger realA = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
            realA.incrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        NoVolatile noVolatile = new NoVolatile();
        Thread thread1 = new Thread(noVolatile);
        Thread thread2 = new Thread(noVolatile);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(noVolatile.a);
        System.out.println(noVolatile.realA.get());
    }
}
複製代碼

  • 適用於直接賦值操做
/**
 * volatile的適用場景
 */
public class UseVolatile implements Runnable {

    volatile boolean done = false;
    AtomicInteger realA = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            setDone();
            realA.incrementAndGet();
        }
    }

    private void setDone() {
        done = true;
    }

    public static void main(String[] args) throws InterruptedException {
        UseVolatile noVolatile = new UseVolatile();
        Thread thread1 = new Thread(noVolatile);
        Thread thread2 = new Thread(noVolatile);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(noVolatile.done);
        System.out.println(noVolatile.realA.get());
    }
}
複製代碼

注意:賦值操做原本是原子操做,因此對volatile修飾的變量進行賦值能夠保證線程安全,可是若是不是直接賦值則沒法保證,請看下面的例子

/**
 * 不適用volatile的場景
 */
public class NoVolatile2 implements Runnable {

    volatile boolean done = false;
    AtomicInteger realA = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            flipDone();
            realA.incrementAndGet();
        }
    }

    private void flipDone() {
        done = !done;
    }

    public static void main(String[] args) throws InterruptedException {
        NoVolatile2 noVolatile = new NoVolatile2();
        Thread thread1 = new Thread(noVolatile);
        Thread thread2 = new Thread(noVolatile);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(noVolatile.done);
        System.out.println(noVolatile.realA.get());
    }
}
複製代碼

上來初始值爲false,因此執行偶數次結果應該爲false,但是執行了20000次以後結果倒是true,從中即可以看出volatile在此狀況下不適用

  • 使用狀況2:做爲刷新變量前的觸發器 這裏可使用前面的例子
/**
 * 演示可見性帶來的問題
 */
public class FieldVisibility {

    int a = 1;
    int abc = 1;
    int abcd = 1;
    volatile int b = 2;

    public static void main(String[] args) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
    }

    private void print() {
        if (b == 0) {
            System.out.println("b = " + b + "; a = " + a);
        }
    }

    private void change() {
        abc = 7;
        abcd = 70;
        a = 3;
        b = 0;
    }
}
複製代碼

在這裏b==0做爲觸發的條件,由於在change()方法中最後一句將b設置爲0,因此依照happens-before原則在b=0之前的操做都是可見的,從而達到了觸發器的做用

7.3 volatile的兩點做用

  • 可見性:讀一個volatile修飾的變量須要使本地緩存失效,而後從主存中讀取新值,寫一個volatile變量後會當即刷回主存
  • 禁止指令重排優化:解決單例雙重鎖亂序問題

7.4 volatile和synchronized的關係

volatile是輕量級的synchronized,當在多線程環境下只作賦值操做時可使用volatile代替synchronized,由於賦值操做自身保證原子性,而使用volatile又能保證可見性,因此能夠實現線程安全。

7.5 volatile總結

  • voaltile修飾符適用於如下場景:當某個屬性被線程共享,其中有一個線程修改了此屬性,其餘線程能夠當即獲得修改後的值,好比boolean flag;,或者做爲觸發器實現輕量級同步。
  • volatile的讀寫都輸無鎖操做,它不能替代synchronized是由於它沒法提供原子性和互斥性,由於無鎖,它也不會在獲取鎖和釋放鎖上有開銷,因此說它是低成本的。
  • volatile只能用於修飾某個屬性,被volatile修飾的屬性不會被重排序
  • volatile提供可見性,任何線程對其進行修改後馬上就會對其餘線程可見,volatile屬性不會被線程線程緩存,必須從主存中讀取
  • volatile提供了happens-before保證
  • volatile能夠保證long和double的賦值操做都是原子的

7.6 能保證可見性的措施

除了volatile能夠保證可見性以外,synchronized、Lock、併發集合、Thread.join()和Thread。start()均可以保證可見性(具體看happens-before原則)。

7.7 對synchronized理解的昇華

  • synchronized不只能夠保證原子性,還能保證可見性
  • synchronized不只讓被保護的代碼安全,並且還「近朱者赤」(具體看happens-before原則)

8.JMM之原子性

8.1 什麼是原子性

一系列操做要麼所有成功,要麼所有失敗,不會出現只執行一半的狀況,是不可分割的。

8.2 Java中原子操做有哪些

  • 除了long和double以外的基本類型的賦值操做。
  • 全部引用的賦值操做
  • java.concurrent.util.Atomic.*包中全部類的原子操做。

8.3 long和double的原子性

對於64位值的寫入,能夠分爲兩個32位操做進行寫入,因此可能會致使64位的值發生錯亂,針對這種狀況能夠添加volatile進行解決。在32位的JVM上它們不是原子的,而在64位的JVM上倒是原子的

8.4 原子操做+原子操做 != 原子操做

簡單的把原子操做組合在一塊兒,並不能保證總體依然具備原子性,好比說去銀行取兩次錢,這兩次取錢都是原子操做,可是中途銀行卡可能會被女友借走,這樣就形成了兩次取錢的中斷。

9.JMM應用實例:單例模式的8種寫法、單例和併發的關係

9.1 單例模式的做用

  • 節省內存和計算
  • 保證結果正確
  • 方便管理

9.2 單例模式的適用場景

  • 無狀態的工具類:好比日誌工具類,無論在哪裏使用,咱們須要的知識讓它幫咱們記錄日誌信息,除此以外,並不須要在它的實例對象上存儲狀態,這時候咱們就只須要一個實例對象便可。
  • 全局信息類:好比在統計網站訪問次數時,咱們不但願一些結果記錄在對象A上,一些記錄在對象B上,此時能夠建立一個單例的類進行計算。

9.3 單例模式的實現

  • 餓漢式(靜態常量,可用)
/**
 * 餓漢式(靜態常量)(可用)
 */
public class Singleton1 {

    private static final Singleton1 INSTANCE = new Singleton1();

    private Singleton1(){

    }

    public Singleton1 getInstance(){
        return INSTANCE;
    }
}
複製代碼
  • 餓漢式(靜態代碼塊,可用)
/**
 * 餓漢式(靜態代碼塊)  (可用)
 */
public class Singleton2 {

    private static Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2(){}

    public static Singleton2 getInstance(){
        return INSTANCE;
    }
}
複製代碼
  • 懶漢式(線程不安全,不可用)
/**
 * 懶漢式(線程不安全)   (不可用)
 */
public class Singleton3 {

    private static Singleton3 INSTANCE;

    private Singleton3(){}

    public static Singleton3 getInstance(){
        if (INSTANCE == null){
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
}
複製代碼

由於這樣寫在多線程狀況下有可能線程1進入了if (INSTANCE == null)但還沒來得及建立,此時線程2進入if (INSTANCE == null),這樣就形成了重複的建立,破壞了單例。

  • 懶漢式(線程安全,同步方法,不推薦用)
/**
 * 懶漢式(線程安全,同步方法)   (不推薦用)
 */
public class Singleton4 {

    private static Singleton4 INSTANCE;

    private Singleton4(){}

    public synchronized static Singleton4 getInstance(){
        if (INSTANCE == null){
            INSTANCE = new Singleton4();
        }
        return INSTANCE;
    }
}
複製代碼

由於添加了synchronized關鍵字,因此能夠保證同一時刻只有一個線程能進入方法也就保證了線程安全。可是因爲添加了synchronized也會對性能產生影響

  • 懶漢式(線程不安全,同步代碼塊,不可用)
/**
 * 懶漢式(線程不安全,同步代碼塊)   (不可用)
 */
public class Singleton5 {

    private static Singleton5 INSTANCE;

    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton5.class) {
                INSTANCE = new Singleton5();
            }
        }
        return INSTANCE;
    }
}
複製代碼

這樣寫看似可行,但是實際上卻不能夠。由於只要INSTANCE爲空就會進入判斷,不管裏面加不加同步遲早都會再次建立,因此這樣會致使實例被屢次建立

  • 雙重檢查
/**
 * 雙重檢查(推薦面試使用)
 */
public class Singleton6 {

    private volatile static Singleton6 INSTANCE;

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton6.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}
複製代碼

優勢:線程安全,延遲加載,效率高

爲何要double-check?單check行不行?

由於若是不進行第二次檢查不管添不添加同步都會對實例進行建立,這樣就會建立多個實例,是線程不安全的

若是把synchronized添加在方法上能夠嗎?

若是添加在方法上是能夠的,可是這樣會形成性能問題

爲何必定要加volatile

由於新建對象不是原子操做,它須要通過建立空對象、調用構造方法、將地址分配給引用這三個步驟,這樣可能會進行重排序,因此就可能出現空指針異常,針對這個問題能夠添加volatile關鍵字來解決

  • 靜態內部類(推薦用)
/**
 * 靜態內部類式,可用
 */
public class Singleton7 {

    private Singleton7() {
    }

    private static class InnerClass{

        //不會對內部靜態實例進行初始化
        private static Singleton7 INSTANCE = new Singleton7();
    }

    public static Singleton7 getInstance(){
        return InnerClass.INSTANCE;
    }
}
複製代碼

靜態內部類方式是一種「懶漢」的方式,在最初對類加載時不會加載內部類的靜態實例

  • 枚舉單例
/**
 * 枚舉單例
 */
public enum Singleton8 {

    INSTANCE;
}
複製代碼

9.4 對比單例模式實現方案

  • 餓漢:簡單,可是沒有lazy loading
  • 懶漢:有線程安全問題
  • 靜態內部類:避免了線程安全問題和資源浪費的問題,可是會增長編程的複雜性
  • 雙重檢查:與JMM相關
  • 枚舉:寫法簡單、先天線程安全、避免反序列化破壞單例
相關文章
相關標籤/搜索