第十二章、 Java內存模型JMM——底層原理

1、到底什麼叫“底層原理”?本章研究的內容是什麼?

1.1 從Java代碼到CPU指令

①最開始,我們編寫的Java代碼,是*.java文件
②在編譯(javac命令)後,從剛纔的*.java文件會變出一個新的Java字節碼文件(*.class)
③JVM會執行剛纔生成的字節碼文件(*.class),並把字節碼文件轉化爲機器指令
④機器指令可以直接在CPU上運行,也就是最終的程序執行

1.2 JVM實現會帶來不同的“翻譯”,不同的CPU平臺的機器指令又千差萬別,無法保證併發安全的效果一致

1.3 因此引入內存模型:轉換過程的規範、原則

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

整體方向:

  • JVM內存結構,和Java虛擬機的運行時區域有關。如堆和棧
  • Java內存模型,和Java的併發編程有關
  • Java對象模型,和Java對象在虛擬機中的表現形式有關

2.1 JVM內存結構

  • 堆(heap):是運行時數據區中佔用最大的。存儲對象的實例
  • 虛擬機棧/Java棧(VM stack):保存各個基本類型、對象引用
  • 方法區(method):存放static的靜態變量/類/常量,以及永久引用
  • 本地方法棧:存放與本地方法(native)相關的
  • 程序計數器

2.2 Java對象模型

  • Java對象自身的存儲模型
  • JVM會給這個類創建一個instanceKlass保存在方法區,用來在JVM層表示該Java類。
  • 當我們在Java代碼中,使用new創建一個對象的時候,JVM會創建一個instanceOopDesc對象,這個對象中包含了對象頭以及實例數據。

3、JMM是什麼

3.1 爲什麼需要JMM

  • C語言不存在內存模型的概念
  • 依賴處理器,不同處理器結果不一樣
  • 無法保證併發安全
  • 需要一個標準,讓多線程運行的結果可預期

3.2 JMM是規範

  • Java Memory Model
  • JMM是一組規範,需要各個JVM的實現來遵守JMM規範,以便於開發者可以利用這些規範,更方便地開發多線程程序
  • 如果沒有這樣的一個JMM內存模型來規範,那麼很可能經過了不同JVM的不同規則的重排序之後,導致不同的虛擬機上運行的結果不一樣,那是很大的問題

3.3 JMM是工具類和關鍵字的原理

  • volatile、synchronized、Lock等的原理都是JMM
  • 如果沒有JMM,那就需要我們自己指定什麼時候用內存柵欄等,那是相當麻煩的,幸好有了JMM,讓我們只需要用同步工具和關鍵字就可以開發併發程序

3.4 最重要的3點內容:重排序、可見性、原子性爲什麼需要JMM

4、重排序

4.1 重排序的代碼案例

/**
 * OutOfOrderExecution
 *
 * @author venlenter
 * @Description: 演示重排序的現象
 * “直到達到某個條件才停止”,測試小概率事件
 * @since unknown, 2020-05-20
 */
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);
    }
}

4.2 重排序分析

這4行代碼的執行順序決定了最終x和y的結果,一共有3種情況:

  • 1、 a=1;x=b(0);b=1;y=a(1),最終結果是x=0,y=1
Thread-one執行完後,Thread-two再執行
  • 2、 b=1;y=a(0);a=1;x=b(1),最終結果是x=1,y=0
Thread-two執行完後,Thread-one再執行
  • 3、 b=1;a=1;x=b(1);y=a(1),最終結果是x=1,y=1
/**
 * OutOfOrderExecution
 *
 * @author venlenter
 * @Description: 演示重排序的現象
 * “直到達到某個條件才停止”,測試小概率事件
 * @since unknown, 2020-05-20
 */
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.countDown();
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a = 1;
                    x = b;
                }
            });
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.countDown();
                        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 + ")";
            System.out.println(result);
            if (x == 1 && y == 1) {
                break;
            }
        }
    }
}
//輸出結果
...
第230次(0,1)
第231次(1,1)
  • 重排序出現的情況(x=0,y=0)
Thread-1
a = 1;
x = b;
——————————————————————————
Thread-2
b = 1;
y = a;


y=a;  #Thread2的2行代碼被重排序了,同時中間插入了Threa1
a=1;
x=b;
b=1

4.3 什麼是重排序:

在線程1內部的兩行代碼的【實際執行順序】和代碼在【Java文件中的順序】不一致,代碼指令並不是嚴格按照代碼語句順序執行的,它們的順序被改變了,這就是重排序,種類被顛倒的是y=a和b=1這2行語句

4.4 重排序的好處:提高處理速度

  • 對比重排序前後的指令優化 

4.5 重排序的3種情況:編譯器優化、CPU指令重排、內存的“重排序”

  • 編譯器優化:包括JVM,JIT編譯器等
  • CPU指令重排:就算編譯器不發生重排,CPU也可能對指令進行重排
  • 內存的“重排序”:線程A的修改線程B卻看不到,引出可見性問題

5、可見性

5.1 案例:演示什麼是可見性問題

(1)案例一

  • 代碼演示
/**
 * FieldVisibility
 *
 * @author venlenter
 * @Description: 演示可見性帶來的問題
 * @since unknown, 2020-05-23
 */
public class FieldVisibility {
    int a = 1;
    int b = 2;
    private void change() {
        a = 3;
        b = a;
    }
    private void print() {
        System.out.println("b=" + b + ";a=" + a);
    }
    public static void main(String[] args) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
    }
}
//輸出可能結果
b=2;a=1(println線程先執行)
b=3;a=3(先執行完全部change,再執行println)
b=2;a=3(change執行a=3後,切換到println線程)


第四種情況:
b=3;a=1(出現了可見性問題--概率比較小,但確實發生了):
沒給b加volatile,那麼有可能出現a=1,b=3.因爲a雖然被修改了,但是其他線程不可見,而b恰好其他線程可見,這就造成了b=3,a=1

(2)案例二

  • 代碼
public class FieldVisibility {
    int x = 0;
    public void writeThread() {
        x = 1;
    }
    public void readerThread() {
        int r2 = x;
    }
}
  • 案例二分析:

  • 可見性問題出現問題原因:
①主內存中原x=0,線程1和線程2分別讀取了x=0
②線程1在工作內存中賦值x=1,但還沒有寫入到主內存
③此時線程2的本地內存中x還是0,所以導致了可見性問題

(3)用volatile解決問題

  • 代碼
/**
 * FieldVisibility
 *
 * @author venlenter
 * @Description: 演示可見性帶來的問題
 * @since unknown, 2020-05-23
 */
public class FieldVisibility {
    //分別對變量加volatile
    volatile int a = 1;
    volatile int b = 2;
    private void change() {
        a = 3;
        b = a;
    }
    private void print() {
        System.out.println("b=" + b + ";a=" + a);
    }
    public static void main(String[] args) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

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

①使用了volatile,線程1在工作內存中修改了x=1後,會強制flush到主內存
②當線程2要讀取x/使用舊的x的時候,會判斷x爲失效,同時重新從主內存中讀取進來,則x爲新的1

5.2、爲什麼會有可見性問題

RAM是主內存
registers是寄存器
core假設是多核CPU
  • CPU讀取寄存器registers中緩存——>registers讀取L1 cache級緩存——>L2——>L3——>主內存RAM

CPU有多級緩存,導致讀的數據過期

  • 高速緩存的容量比主內存小,但是速度僅次於寄存器,所以在CPU和主內存之間就多了Cache層
  • 線程間的對於共享變量的可見性問題不是直接由多核引起的,而是由多緩存引起的
  • 如果所有的核心(core)都只用一個緩存,那麼也就不存在內存可見性問題了
  • 每個核心都會將自己需要的數據讀到獨佔緩存(工作內存)中,數據修改後也是寫入到獨佔緩存中,然後等待刷入到主存中。所以會導致有些核心讀取的值是一個過期的值

5.3、JMM的抽象:主內存和本地內存

5.3.1 什麼是主內存和本地內存

  • Java作爲高級語言,屏蔽了這些底層細節,用JMM定義了一套讀寫內存數據的【規範】,雖然我們【不再需要關心一級緩存和二級緩存】的問題,但是,JMM抽象了主內存和本地內存的概念
  • 這裏說的本地內存【並不是真的是一塊給每個線程分配的內存】,而是JMM的一個抽象,是對於寄存器、一級緩存、二級緩存等的【抽象】
  • 以上面core、regisiters的那張圖來說:(registers、L1、L2是線程的本地內存)(L3、RAM是線程共享的)

5.3.2 主內存和本地內存的關係

JMM有以下規定:

  • 【所有的變量】都存儲在【主】內存中,同時【每個線程】也有自己【獨立】的【工作內存】,工作內存中的變量內容是主內存中的【拷貝】
  • 線程【不能直接讀寫主內存中】的變量,而是隻能【操作自己工作內存】中的變量,然後再【同步】到主內存中
  • 【主內存】是【多個線程共享】的,但【線程間不共享工作內存】,如果線程間需要【通信】,必須藉助【主內存中轉】來完成

所有的【共享變量存在於主內存】中,每個【線程有自己的本地內存】,而且【線程讀寫共享數據也是通過本地內存交換】的,所以才導致了【可見性問題】

5.4、Happens-Before原則

  • 什麼是happens-before:(解決可見性問題)在時間上,動作A發生在動作B之前,B保證能看見A,這就是happens-before
  • Happens-Before原則有哪裏?

1. 單線程規則

  • 同個線程(同個工作內存)內,前面修改的變量對後面的操作是可見的。但不影響重排序
  • 例如
 int a = 1;
    int b = 2;
    private void change() {
        a = 3;
        b = a;
    }
    
在同個線程執行change()的時候,a=3先執行,則後面b=a肯定爲3,因爲這裏使用到的是同個工作內存,a=3和b=3都是在該線程的同個工作內存中執行的,所以可見

但如果發生了重排序,也就是b=a先執行了,這是允許的,這裏b=a=1初始值

2. 鎖操作(synchronized和Lock)

3. volatile變量

  • 理解:只要TheadA volatile變量是已經寫入了,那麼ThreadB讀取就肯定可以讀取到最新的結果
  volatile int a = 1;
    volatile int b = 2;
    private void change() {
        a = 3;
        b = a;
    }
    private void print() {
        System.out.println("b=" + b + ";a=" + a);
    }
    
如果change()先執行,由於a、b被volatile,volatile也會防止重排序導致錯誤結果,所以a=3肯定在b=a之前執行。

所以當change()先執行完後,再執行print(),肯定可以得到正確的結果,不會出現change線程和print線程穿插導致變量值錯亂的問題

4. 線程啓動

  • ThreadB執行的時候,可以看到ThreadA之前的操作

5. 線程join

  • 在ThreadA(例如主線程爲main()方法)中執行了ThreadB.join,則ThreadA會等待ThreadB執行完畢後,才執行下面的statement1的操作邏輯
  • 當ThreadB執行完畢後,下面的statement1也可以看到statement1的變化

6. 傳遞性:如果hb(A,B)而且hb(B,C),那麼可以推出hb(A,C)

  • 假設背景爲main主線程中有ThreadA、ThreadB、ThreadC的執行,如果ThreadA和ThreadB遵循happen-before原則,ThreadB和ThreadC也遵循happens-before,則可以推出hb(A,C)

7. 中斷:一個線程被其他線程interrupt時,那麼檢測中斷(isInterrupted)或者拋出InterruptedException一定能看到

8. 構造方法:對象構造方法的最後一行指令happens-before於finalize()方法的第一行指令

  • finalize()已不推薦使用

9. 工具類的Happens-Before原則

  • (1)線程安全的容器get一定能看到在此之前的put等存入動作
如線程安全的ConcurrentHashMap的get和put
  • (2)CountDownLatch
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            latch.countDown();
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a = 1;
        x = b;
    }
});
one.start();
latch.countDown();

當執行CountDownLatch的countDown(),Thread one才能從await中喚醒,繼續執行下面的a=1;x=b
  • (3)Semaphore:類似CountDownLatch
  • (4)Future:可以去後臺執行,並拿到一個線程執行結果的類。Future的get是拿到Future的執行結果,get對於之前的執行結果是可見的(不用過多關注,默認保證的)
  • (5)線程池:我們會向線程池提交許多任務,然後在提交的任務中,每個任務都可以看到在提交之前的所有的執行結果(不用過多關注,默認保證的)
  • (6)CyclicBarrier:CountDownLatch
①CyclicBarrier cyclicBarrier1 = new CyclicBarrier(1);
②cyclicBarrier1.await();
③xxx
④cyclicBarrier1.reset(); //當執行了reset後,才能從②await中喚起,繼續執行③的代碼

案例:happens-before演示

  • happens-before有一個原則是:如果A是對volatile變量的寫操作,B是對同一個變量的讀操作,那麼hb(A,B)
  • 分析這四種情況:
    int a = 1;
    int b = 2;
    private void change() {
        a = 3;
        b = a;
    }
    private void print() {
        System.out.println("b=" + b + ";a=" + a);
    }
    
//輸出可能結果
b=2;a=3(change執行a=3後,切換到println線程)
b=2;a=1(println線程先執行)
b=3;a=3(先執行完全部change,再執行println)

第四種:b=3;a=1(出現了可見性問題--概率比較小,但確實發生了):
沒給b加volatile,那麼有可能出現a=1,b=3.因爲a雖然被修改了,但是其他線程不可見,而b恰好其他線程可見,這就造成了b=3,a=1
  • 改進:之前是對a、b都加了volatile,實際上在該場景,只要對b加volatile就可以了
  int a = 1;
    volatile int b = 2;
    private void change() {
        a = 3;
        b = a;
    }
    private void print() {
        System.out.println("b=" + b + ";a=" + a);
    }
  • 近朱者赤:給b加了volatile,不僅b被影響,也可以實現輕量級同步
  • b之前的寫入(對應代碼b=a)對讀取b後的代碼(print b)都可見,所以在writerThread裏對a的賦值,一定會對readerThread裏的讀取可見,所以這裏的【a即使不加volatile,只要b讀到的是3,就可以由happens-before原則保證了print a讀到的也都是3而不可能讀到1

5.5、volatile關鍵字

5.5.1 volatile是什麼

  • volatile是一種【同步機制】,比synchronized或者Lock相關類【更輕量】,因爲使用volatile並不會發生【上下文切換】等開銷很大的行爲
  • 如果一個變量被修飾成volatile,那麼JVM就知道了這個變量可能【會被併發修改】(JVM就會做一些相關邏輯,如禁止重排序)
  • 開銷小,相應的能力也小,雖然說volatile是用來同步地保證線程安全的,但volatile無法保證synchronized那樣的【原子保護】,volatile僅在【很有限的場景】下才能發揮作用

5.5.2 volatile的適用場合

(1)不適用a++

/**
 * NoVolatile
 *
 * @author venlenter
 * @Description: 不適用於volatile的場景
 * @since unknown, 2020-05-26
 */
public class NoVolatile implements Runnable {
    volatile int a;
    AtomicInteger realA = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Runnable r = new NoVolatile();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((NoVolatile) r).a);
        System.out.println(((NoVolatile) r).realA);

    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
            realA.incrementAndGet();
        }
    }
}
//輸出結果
19855
20000

(2)適用場景:volatile的變量,不依賴之前的值。如果依賴之前的值如a++(先讀a,再+),就會有問題。如果只是對變量進行覆蓋賦值(不依賴之前的值),則適用

(3)適用場景1:boolean flag,如果一個共享變量自始自終只【被各個線程賦值】,而沒有其他的操作(對比、取值),那麼就可以用volatile來代替synchronized或者代替原子變量,因爲賦值自身是有原子性的,而volatile又保證了可見性,所以就足以保證線程安全1

(4)適用場景2:作爲刷新之前變量的觸發器

Map configOptions;
char[] configText;
volatile boolean initialized = false;

//Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;

//Thread B
## 當在ThreadA中initialized設置爲true,則在ThreadB中就跳過while,同時因爲volatile的happens-before,則在ThreadA的initialized賦值操作前的configOptions肯定已經初始化完畢了
while (!initialized) {
    sleep();
}
//use configOptions

5.5.3 volatile的作用:可見性、禁止重排序

(1)可見性:讀volatile變量時會去【主內存讀取最新值】,寫一個volatile屬性會【立即刷入到主內存】

(2)禁止指令【重排序】優化:解決單例雙重鎖亂序問題

5.5.4 volatile和synchronized的關係?

  • volatile可看做是【輕量版的synchronized】:如果一個共享變量自始至終【只被各個線程賦值】,而沒有其他的操作(讀值),那麼就可以用volatile來代替synchronized或者代替原子變量,因爲【賦值自身是有原子性的,而volatile又保證了可見性】,所以就足以保證線程安全

5.5.5 學以致用:用volatile修正重排序問題

  • OutOfOrderExecution類加了volatile後,用於不會出現(0,0)的情況了
/**
 * OutOfOrderExecution
 *
 * @author venlenter
 * @Description: 演示重排序的現象
 * “直到達到某個條件才停止”,測試小概率事件
 * @since unknown, 2020-05-20
 */
public class OutOfOrderExecution {
    //加上volatile,則不會出現(0,0)了
    private volatile static int x = 0, y = 0;
    private volatile 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.countDown();
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a = 1;
                    x = b;
                }
            });
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.countDown();
                        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 + ")";
            System.out.println(result);
            if (x == 0 && y == 0) {
                break;
            }
        }
    }
}

5.5.6 volatile小結

  • 1、volatile修飾符【適用於以下場景】:某個屬性被多個線程共享,其中一個線程修改了此屬性,其他線程可以立即得到修改後的值,比如【boolean flag】;或者作爲【觸發器】,實現輕量級同步
  • 2、volatile屬性的書寫操作都是【無鎖】的,它不能替代synchronized,因爲它沒有提供【原子性】和【互斥性】。因爲無鎖,不需要花費時間在獲取鎖和釋放鎖上,所以說它是【低成本】的
  • 3、volatile只能作用於【屬性】。使用volatile修飾屬性,該屬性就不會被指令重排序
  • 4、volatile提供了【可見性】,任何一個線程對其的修改將立馬對其他線程可見。volatile屬性不會被線程緩存,始終【從主存中讀取】
  • 5、volatile提供了【happens-before】保證,對volatile變量v的寫入操作-【happens-before】於-所有其他線程後續對v的讀操作
  • 6、volatile可以【使得long和double的賦值是原子】的

5.6、能保證可見性的措施

  • 除了volatile可以讓變量保證可見性外,【synchronized、Lock、併發集合、Thread.join()和Thread.start()】等都可以保證可見性
  • 具體看happens-before原則的規定

5.7、昇華:對synchronized可見性的正確理解

  • synchronized不僅保證了原子性,還保證了【可見性】
  • synchronized不僅讓被保護的代碼安全,還讓其之前的代碼執行結果可見

6、原子性

6.1、什麼是原子性

  • 一系列操作,要麼全部執行成功,要麼全部不執行,不會出現執行一半的情況,是不可分割的
  • 銀行轉賬問題(A轉賬給B):A先減100,B再加100
  • i++不是原子性的
  • 用synchronized實現原子性

6.2、Java中的原子操作有哪些?

  • 除long和double之外的【基本類型】(int,byte,boolean,short,char,float)的賦值操作
  • 所有引用【reference的賦值操作】,不管是32位的機器還是64位的機器
  • java.concurrent.Atomic.* 包中所有類的原子操作

6.3、long和double的原子性

  • 問題描述:官方文檔、對於64位的值的寫入,可以分爲兩個32位的操作進行寫入、讀取錯誤、使用volatile解決 https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7
  • 結論:在32位上的JVM上,long和double的操作不是原子的,但在64位的JVM上是原子的
  • 實際開發中:商用Java虛擬機中不會出現

6.4、原子操作+原子操作!=原子操作

  • 簡單地把原子操作組合在一起,並不能保證整體依然具有原子性
  • 全同步的HashMap也不能完全安全(多個synchronized方法操作組合在一起,就不是原子的了)

7、面試常見問題

7.1 JMM應用實例:單例模式8種寫法、單例和併發的關係(真實面試超高頻考點)

(1)單例模式的作用和使用場景

單例模式的作用

  • 爲什麼需要單例模式:節省內存和計算、保證結果正確、方便管理

單例模式適用場景

  • 無狀態的工具類:比如日誌工具類,不管是在哪裏適用,我們需要的只是它幫我們記錄日誌信息,除此之外,並不需要在它的實例對象上存儲任何狀態,這個時候我們就只需要一個實例對象即可
  • 全局信息類:比如我們在一個類上記錄網站的訪問次數,我們不希望有的訪問被記錄在對象A上,有的卻記錄在對象B上,這時候我們就讓這個類成爲單例

(2)單例模式的8種寫法

1、餓漢式(靜態常量)[可用]

/**
 * Singleton1
 *
 * @author venlenter
 * @Description: 餓漢式(靜態常量)(可用)
 * @since unknown, 2020-06-02
 */
public class Singleton1 {
    //類加載時就完成了初始化
    private final static Singleton1 INSTANCE = new Singleton1();
    private Singleton1() {
        
    }
    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

2、餓漢式(靜態代碼塊)[可用]

/**
 * Singleton2
 *
 * @author venlenter
 * @Description: 餓漢式(靜態代碼塊)(可用)
 * @since unknown, 2020-06-02
 */
public class Singleton2 {
    //類加載時就完成了初始化
    private final static Singleton2 INSTANCE;
    static {
        INSTANCE = new Singleton2();
    }
    private Singleton2() {

    }
    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}

3、懶漢式(線程不安全)[不可用]

/**
 * Singleton3
 *
 * @author venlenter
 * @Description: 懶漢式(線程不安全)
 * @since unknown, 2020-06-03
 */
public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {

    }

    public static Singleton3 getInstance() {
        //如果2個線程同時執行到這一行,則會執行2次new Singleton3(),創建了多個實例
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}

4、懶漢式(線程安全,同步方法)[不推薦用]

/**
 * Singleton4
 *
 * @author venlenter
 * @Description: 懶漢式(線程安全)(不推薦)
 * @since unknown, 2020-06-03
 */
public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4() {

    }

    //synchronized,多個線程執行會阻塞等待
    public synchronized static Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }
}

5、懶漢式(線程不安全,同步代碼塊)[不可用]

/**
 * Singleton5
 *
 * @author venlenter
 * @Description: 懶漢式(線程不安全,同步代碼塊)(不可用)
 * @since unknown, 2020-06-03
 */
public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5() {

    }

    public static Singleton5 getInstance() {
        //2個線程同時進入這裏,則A線程synchronized執行完後,B線程又執行一次synchronized初始化
        //本質還是執行了2次初始化,線程不安全
        if (instance == null) {
            synchronized (Singleton5.class) {
                instance = new Singleton5();
            }
        }
        return instance;
    }
}

6、雙重檢查[推薦用]

/**
 * Singleton6
 *
 * @author venlenter
 * @Description: 雙重檢查(推薦使用)
 * @since unknown, 2020-06-03
 */
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行不行?:不行多個線程同時執行到instance==null,雖然有synchronized,但也會執行了2次初始化
③直接在方法加synchronized呢?:性能問題,多個線程排隊等待
  • 爲什麼要用volatile
①新建對象實際上有3個步驟:創建空對象、空對象內初始化、賦值
②重排序會帶來nullpointexception
③防止重排序

7、靜態內部類[推薦使用]

/**
 * Singleton7
 *
 * @author venlenter
 * @Description: 靜態內部類方式,可用
 * @since unknown, 2020-06-03
 */
public class Singleton7 {

    private Singleton7() {

    }
    //JVM加載Singleton7類的時候,不會初始化內部類變量,達到了懶加載
    private static class SingletonInstance {
        private static final Singleton7 instance = new Singleton7();
    }

    public static Singleton7 getInstance() {
        //只有當調用到的是,纔會進行加載
        return SingletonInstance.instance;
    }
}

8、枚舉[推薦用]

/**
 * Singleton8
 *
 * @author venlenter
 * @Description: 枚舉單例
 * @since unknown, 2020-06-04
 */
public enum  Singleton8 {
    INSTANCE;
    public void whatever() {

    }
}

//調用
Singleton8.INSTANCE.whatever();

(3)不同寫法對比

  • 餓漢:簡單,但是沒有lazy loading,直接就初始化創建了一些對象,而這些對象可能是不需要的
  • 懶漢:寫法複雜,同時有線程安全問題
  • 靜態內部類:可用
  • 雙重檢查:同時做到了線程安全和懶加載
  • 枚舉:最好

(4)用哪種單例的實現方案最好?

《Effective Java》中表明:使用枚舉實現單例的方法雖然還沒有廣泛採用,但單元素的枚舉類型已經成爲實現Singleton的最佳方法

  • 寫法簡單
  • 線程安全有保障
  • 避免反序列化破壞單例

(5)各種寫法的適用場景

  • 最好的方法是利用【枚舉】,因爲還可以防止反序列化重新創建新的對象
  • 非線程同步的方法不能使用
  • 如果程序一開始要加載的資源太多,那麼就應該使用【懶加載】
  • 餓漢式如果是對象的創建需要配置文件就不適用(假設對象的創建需要調用一個前置方法去獲取配置,但因爲餓漢式,對象被提前創建,而沒有將對應的前置方法數據賦值進去,造成創建的對象是一個空對象)
  • 懶加載雖然好,但是靜態內部類這種方式會引入編程複雜性

7.2 講一講什麼是Java內存模型

  • JMM是什麼?:一組規範
  • 最重要的3點內容:重排序、可見性、原子性
  • 可見性內容從主內存和本地內存、Happens-before原則、volatile
  • 原子性:實現原子性的方法、單例模式

7.3 volatile和synchronized的異同?

  • volatile可以算是輕量版的synchronized,開銷小,適用場合相對就少一點:如果一個共享變量至始至終只被各個線程賦值,而沒有其他的操作,那麼就可以用volatile來代替

7.4 什麼是原子操作?Java中有哪些原子操作?生成對象的過程是不是原子操作?

  • 什麼是原子操作:要麼全部執行,要麼全部不執行
  • Java中有哪裏原子操作
除long和double之外的【基本類型】(int,byte,boolean,short,char,float)的賦值操作
所有引用【reference的賦值操作】,不管是32位的機器還是64位的機器
java.concurrent.Atomic.* 包中所有類的原子操作
  • 生成對象的過程是不是原子操作:是多步操作,無法保證原子操作
①新建一個空的Person對象
②執行Person的構造函數
③把這個對象的地址指向p

7.5 什麼是內存可見性?

7.6 64位的double和long寫入的時候是原子的嗎

  • 32位上不是原子的,64位上是原子的,一般不需要我們考慮

8、總結:Java內存模型————底層原理

  • 什麼叫“底層原理”
  • 三兄弟:JVM內存結構 VS Java內存模型 VS Java對象模型
  • JMM是什麼
  • 重排序
  • 可見性
  • 原子性

筆記來源:慕課網悟空老師視頻《Java併發核心知識體系精講》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章