單例模式深度分析

概述

單例模式,它能保證我們始終如一的使用同一個對象,我們平時經常會去用它,因爲可以避免重複製造對象,減少內存隱患,我們也都可以寫個常見的單例出來。
這裏要講下單例到底應該怎麼寫,既能避免線程不安全,也能保證性能。

內容

1、一個最簡單的單例模式

public class GirlFriend {

    //靜態變量記錄唯一實例
    private static GirlFriend instance;

    //構造器私有化
    private GirlFriend() {
    }

    //靜態實例化方法公有,保證不重複創建對象
    public static GirlFriend getInstance() {
        if (instance == null) {
            instance = new GirlFriend();
        }
        return instance;
    }
}

以上代碼片,給出了單例模式的最基本的三要素:

  • 靜態變量記錄唯一實例;
  • 構造器私有化;
    這樣能保證外界不會通過構造器構造對象。
  • 靜態實例化方法公有化,保證不重複創建對象
    void test() {
        GirlFriend girlFriend = GirlFriend.getInstance();
    }

當你在第一次調用getInstance()方法時,會對instance進行初始化,以後再次調用該方法,則不會重複創建對象,而是直接使用已經被實例化的instance對象。
但是呢,這個單例不是線程安全的,假設有兩個線程,都在調用getInstance()方法,其中線程A執行到了“if (instance == null) ”,而另一個線程B執行到“instance = new GirlFriend();”,那麼當B中的instance實例初始化完成前,線程A就已經滿足條件即將進入再次初始化instance,這樣一來,instance實例被初始化了兩次,且A、B兩個線程得到的並不是同一個實例對象,如果同時一百個線程這麼執行,將構造一百次GirlFriend類型變量,所以多線程使用以上單例就需要謹慎了

2、同步機制

如果想要線程安全,一種最便捷的方式是使用同步“synchronized”,就像這樣:

public class GirlFriend {

    private static GirlFriend instance;

    private GirlFriend() {
    }

    public static synchronized GirlFriend getInstance() {
        if (instance == null) {
            instance = new GirlFriend();
        }
        return instance;
    }
}

使用synchronized關鍵字修飾getInstance()方法,會使得,在多個線程執行此方法時,必須等待其他線程執行完畢後,才能進入該方法,且每次只能有一個線程進入getInstance()方法
但是,頻繁併發同步時,會意味着,你多個線程總是要等來等去,一個接一個執行被synchronized關鍵字修飾的代碼,這樣會影響程序效率、性能。我們只是需要在第一次進入getInstance()方法時,同步線程避免多次實例化對象。

雙重檢查加鎖

爲了避免多次同步,使用雙重檢查加鎖,如下:

public class GirlFriend {

    private volatile static GirlFriend instance;

    private GirlFriend() {
    }

    public static GirlFriend getInstance() {
        if (instance == null) {
            synchronized (GirlFriend.class) {
                if (instance == null) {
                    instance = new GirlFriend();
                }
            }
        }
        return instance;
    }
}

分析一下,如果同時線程A和線程B幾乎同時執行getInstance()方法,它們都經歷第一次條件判斷,假設A拿到同步鎖,執行初始化instance的代碼,線程B等候A釋放了同步鎖後,經歷第二次判斷,顯然instance已經不爲空了,所以會釋放鎖,執行“return instance”,避免了重複初始化instance。這樣以後再調用getInstance()方法,第一次條件判斷爲“假”,則避免了synchronized語句,極大的優化了性能。
上面那段話橫豎沒有提到“volatile”關鍵字,但是爲神馬要用它呢?

計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程中,勢必涉及到數據的讀取和寫入。由於程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由於CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度。因此在CPU裏面就有了高速緩存。也就是,當程序在運行過程中,會將運算需要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之後,再將高速緩存中的數據刷新到主存當中。舉個簡單的例子,比如下面的這段代碼:
i = i + 1;
當線程執行這個語句時,會先從主存當中讀取i的值,然後複製一份到高速緩存當中,然後CPU執行指令對i進行加1操作,然後將數據寫入高速緩存,最後將高速緩存中i最新的值刷新到主存當中。
在多核CPU中,每個線程都有自己的高速緩存,這就意味着,在多個線程併發讀寫同一個變量時,這種讀寫只是作用於高速緩存中內容。比如以上那句代碼,當線程A和線程B分別從主內存中讀取i的值到自己的CPU高速緩存中,A中的“i=i+1”,則“i”值爲“1”,這個值還在A線程CPU高速緩存中,沒有同步到主內存,而此時線程B正好執行了“i=i+1”,但因爲“i”存在於B線程CPU高速緩存中的值仍然是0,執行“i=i+1”的結果仍然是“i”值爲“1”,最後兩線程同步到主內存中的i的值是“1”。但實際上我們預測的是“i加1然後再加1“,結果是“2”。

volatile:它是一個類型修飾符(type specifier),就像大家更熟悉的const一樣,它是被設計用來修飾被不同線程訪問和修改的變量。volatile的作用是作爲指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值。
這就意味着,被volatile修飾的變量,值已經修改,會馬上同步到主內存。所以多個線程每次得到的被volatile修飾的變量,它的值都是經過最近更新的。

所以這裏我們用volatile修飾instance變量,就是爲了讓instance的值和主內存同步

總結

如果這篇博客幫助到您,您可以完成以下操作:
在網易雲搜索“星河河”->歌手->點擊進入(您將進入我的網易雲音樂人賬號星河河)->關注我->多聽聽我的歌。

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