多線程下指令重排與DCL單列模式

指令重排簡述

1、JMM內(nèi)存模型三大特性包括原子性,可見性,有序性。詳細請看關(guān)于Java內(nèi)存模型的三大特性

2、指令重排是相對有序性來說的,指在程序執(zhí)行過程中, 為了性能考慮, 編譯器和CPU可能會對指令重新排序。單線程模式下只有一個執(zhí)行引擎,不存在競爭,所有的操作都是有有序的,不影響最后的執(zhí)行結(jié)果。

3、指令重排只能保證串行(單線程)語句執(zhí)行的一致性。

單例模式

假設(shè)我的單列對象是Faith(一個人只有一個信仰),查看多線程下示例的創(chuàng)建次數(shù),即構(gòu)造函數(shù)的調(diào)用次數(shù)。

餓漢模式

示例代碼

class Faith {
    private static Faith myFaith = new Faith();
    private Faith(){
        System.out.println("Faith.Faith --- 私有構(gòu)造調(diào)用了");
    }

    public static Faith getMyFaith() {
        return myFaith;
    }
}

public class TestSingleton {
    public static void main(String[] args) {
        for (int i = 0; i <= 10; i++) {
            new Thread(() -> {
                Faith.getMyFaith();
            },String.valueOf(i)).start();
        }
    }
}

控制臺:

Faith.Faith --- 私有構(gòu)造調(diào)用了
  • 多條線程同時運行時,只創(chuàng)建了一個實例。
  • 餓漢模式下,在類加載的時候創(chuàng)建一次實例,不會存在多個線程創(chuàng)建多個實例的情況。但在類加載時就自動創(chuàng)建,占用內(nèi)存。
  • 因此重點講懶漢模式,即第一次調(diào)用獲取實列方法時,才被動創(chuàng)建對象。

懶漢模式

單線程懶漢模式

示例代碼

class Faith {
    private static Faith myFaith = null;
    private Faith(){
        System.out.println(Thread.currentThread().getName()+" --- Faith.Faith --- 私有構(gòu)造調(diào)用了");
    }

    public static Faith getMyFaith() {
        if (myFaith == null){
             myFaith =  new Faith();
        }
        return myFaith;
    }
}

上面的代碼是單線程下的懶漢模式,但是在并發(fā)情況下,當myFaith為空,需new對象時,多個線程可能同時進入這個方法。

public class TestSingleton {
    public static void main(String[] args) {
        for (int i = 0; i <= 10; i++) {
            new Thread(() -> {
                Faith.getMyFaith();
            },String.valueOf(i)).start();
        }
    }
}

控制臺:

5 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
1 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
8 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
4 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
2 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
3 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
9 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
7 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
10 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
0 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
6 --- Faith.Faith --- 私有構(gòu)造調(diào)用了

可以看到,結(jié)果非常糟糕,得到多個不同對象。

多線程懶漢模式-synchronized

最直接的方法就是在靜態(tài)方法上加synchronized互斥鎖.

public static synchronized Faith getMyFaith() {
    if (myFaith == null){
        myFaith =  new Faith();
    }
    return myFaith;
}

synchronized屬于重量鎖,在高并發(fā)情況下,上百條個線程都等在靜態(tài)方法外,阻塞很大,不推薦。

多線程懶漢模式-DCL

DCL(double check lock)雙端檢索機制,在new方法上加同步鎖,但要在加鎖前后進行非空判斷。

class Faith {
    private static Faith myFaith = null;
    private Faith(){
        System.out.println(Thread.currentThread().getName()+" --- Faith.Faith --- 私有構(gòu)造調(diào)用了");
    }

    public static Faith getMyFaith() {
        // 第一次判斷,若myFaith實例為空
        if (myFaith == null){
            // 加同步鎖
            synchronized (Faith.class) {
                // 第二次判斷,若myFaith實例確實為空,進入構(gòu)造方法
                if (myFaith == null) {
                    myFaith = new Faith();
                }
            }
        }
        return myFaith;
    }
}
public class TestSingleton {
    public static void main(String[] args) {
        for (int i = 0; i <= 10; i++) {
            new Thread(() -> {
                Faith.getMyFaith();
            },String.valueOf(i)).start();
        }
    }
}

控制臺:

0 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
  • 可以看到,10條線程下,只獲取到一個實列對象,看似是一個相對高效的方法。但在本文一開始,就提到了指令重排。
  • 當myFaith為空,進入初始化,當還沒初始化完成時,會有線程安全問題。

指令重排分析

myFaith = new Faith();,該方法其實有3步:

1、分配內(nèi)存空間何內(nèi)存地址

memeory = allocate;

2、初始化對象

myFaith(memory);

3、將實例指向分配的內(nèi)存地址

myFaith = memory;

第二步和第三步?jīng)]有數(shù)據(jù)依賴關(guān)系,單線程下指令重排不影響執(zhí)行結(jié)果,因此編譯器和cpu允許重排優(yōu)化的行為。

即可能出現(xiàn)第三步先于第二部執(zhí)行, myFaith = memory; 此時因為已經(jīng)給即將創(chuàng)建的myFaith分配了內(nèi)存空間,所以myFaith!=null,但對象的初始化還沒有完成,造成線程安全問題。

多線程懶漢模式-DCL+volatile

JMM保證有序性的重要方法就是引入J.U.C并發(fā)包下的volatile關(guān)鍵字,volatile 關(guān)鍵字通過添加內(nèi)存屏障的方式來禁止指令重排,即重排序時不能把后面的指令放到內(nèi)存屏障之前。

即原來的DCL單例模式,在實例對象上再加volatile修飾即可。

private static volatile Faith myFaith = null;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 以上代碼會重復(fù)運行 , 不會停止。 JMM(java內(nèi)存模型) 若想學(xué)習(xí)好多線程, 那么必須了解一下JMM Jav...
    尼爾君閱讀 1,820評論 0 2
  • 1 基本概念 在上一篇文章Java內(nèi)存區(qū)域 中,我們講了JVM為了更好的管理內(nèi)存,將Java進程的內(nèi)存劃分成...
    yeonon閱讀 463評論 0 0
  • Java SE 基礎(chǔ): 封裝、繼承、多態(tài) 封裝: 概念:就是把對象的屬性和操作(或服務(wù))結(jié)合為一個獨立的整體,并盡...
    Jayden_Cao閱讀 2,234評論 0 8
  • layout: posttitle: 《Java并發(fā)編程的藝術(shù)》筆記categories: Javaexcerpt...
    xiaogmail閱讀 6,005評論 1 19
  • 九種基本數(shù)據(jù)類型的大小,以及他們的封裝類。(1)九種基本數(shù)據(jù)類型和封裝類 (2)自動裝箱和自動拆箱 什么是自動裝箱...
    關(guān)瑋琳linSir閱讀 2,049評論 0 47

友情鏈接更多精彩內(nèi)容