面試:線程安全的惡漢單例

📖摘要


今天分享下 —— 手寫一個線程安全的惡漢單例 的一些基本知識,歡迎關注!

網上都說單例模式是所有模式中最簡單的一種模式,巧的是我也這麼認爲。不過越簡單的東西,往往坑就隱藏的越深,這邊文章我會把我知道的幾個坑講出來。


🌂分享


什麼是單例模式

就如同他的名字一樣,‘單例’-就是隻有一個實例。也就是說一個類在全局中最多隻有一個實例存在,不能在多了,在多就不叫單例模式了。

  1. 小故事
  • 程序員小H單身已久,每天不是對着電腦,就是抱着手機這樣來維持生活。某日,坐在電腦前,突然感覺一切都索然無味。謀生想找一個對象來一起度過人生美好的每一天。
  • 於是精心打扮出門找對象,由於小H很帥,很快就找到了心儀的另一半–小K。小H的心中永遠只有小K一個人,而且發誓永遠不會在找新對象。
  • 小H和小K的關係就是單例模式,在小H的全局中只有一個小K對象,且無第二個,如果有第二個的話,他們之間的關係就出問題了。哈哈
  1. 用在哪裏
    單例模式一般用在對實例數量有嚴格要求的地方,比如數據池,線程池,緩存,session 回話等等。

  2. 在Java中構成的條件

  • 靜態變量
  • 靜態方法
  • 私有構造器

單例模式的兩種形態

懶漢模式

線程不安全

public class Singleton {

    private static Singleton unsingleton;

    private Singleton(){}

    public static Singleton getInstance(){
        if(unsingleton==null){
            unsingleton=new Singleton();
        }
        return unsingleton;
    }
}
餓漢模式

線程安全

public class Singleton {

    private static Singleton unsingleton=new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return unsingleton;
    }
}

調用

public class Test {
    public static void main(String[] args) {
        Singleton singleton1=Singleton.getInstance();
    }
}

懶漢模式優化成線程安全

懶漢模式要變成線程安全的除了用餓漢模式之外,還有兩種方法。

加synchronized關鍵字

此方法是最簡單又有效的方法,不過對性能上會有所損失。比如兩個線程同時調用這個實例,其中一個線程要等另一個線程調用完纔可以繼續調用。而線程不安全往往發生在這個實例在第一次調用的時候發生,當實例被調用一次後,線程是安全的,所以加 synchronized 就顯得有些浪費性能。

public class Singleton {

    private static Singleton unsingleton;

    private Singleton(){}

    public static synchronized Singleton getInstance(){
        if(unsingleton==null){
            unsingleton=new Singleton();
        }
        return unsingleton;
    }
}
用"雙重檢查加鎖"

上個方法說到,線程不安全往往發生在這個實例在第一次調用的時候發生,當實例被調用一次後,線程是安全的。那有沒有方法只有在第一次調用的時候才用 synchronized 關鍵字,而第一次後就不用 synchronized 關鍵字呢?答案是當然有的,就是用 volatile 來修飾靜態變量,保持其可見性。

public class Singleton {

    private static volatile Singleton unsingleton;

    private Singleton(){}

    public static Singleton getInstance(){
        if(unsingleton==null){
            //只有當第一次訪問的時候纔會使用synchronized關鍵字
            synchronized (Singleton.class){
                unsingleton=new Singleton();
            }
        }
        return unsingleton;
    }
}
用"靜態內部類"

靜態內部類的優點是:外部類加載時並不需要立即加載內部類,內部類不被加載則不去初始化 INSTANCE,故而不佔內存。即當 SingleTon 第一次被加載時,並不需要去加載 SingleTonHoler,只有當 getInstance() 方法第一次被調用時,纔會去初始化 INSTANCE,第一次調用 getInstance() 方法會導致虛擬機加載 SingleTonHoler 類,這種方法不僅能確保線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。

public class Singleton {
    private Singleton() {
      }

      private static Singleton instatnce;

      private static class SingletonHolder {
        private static Singleton instatnce = new Singleton();
      }

      public static Singleton getInstance() {
        return SingletonHolder.instatnce;
      }
	}

那麼,靜態內部類又是如何實現線程安全的呢?首先,我們先了解下類的加載時機。

類加載時機:JAVA虛擬機在有且僅有的5種場景下會對類進行初始化。

  1. 遇到 new、getstatic、setstatic或者invokestatic 這4個字節碼指令時,對應的java代碼場景爲:new 一個關鍵字或者一個實例化對象時、讀取或設置一個靜態字段時( final 修飾、已在編譯期把結果放入常量池的除外)、調用一個類的靜態方法時。

  2. 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒進行初始化,需要先調用其初始化方法進行初始化。

  3. 當初始化一個類時,如果其父類還未進行初始化,會先觸發其父類的初始化。

  4. 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含 main() 方法的類),虛擬機會先初始化這個類。

  5. 當使用 JDK 1.7 等動態語言支持時,如果一個java.lang.invoke.MethodHandle 實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

這5種情況被稱爲是類的主動引用,注意,這裏《虛擬機規範》中使用的限定詞是"有且僅有",那麼,除此之外的所有引用類都不會對類進行初始化,稱爲被動引用。靜態內部類就屬於被動引用的行列。

我們再回頭看下 getInstance() 方法,調用的是SingleTonHoler.INSTANCE,取的是 SingleTonHoler 裏的 INSTANCE 對象,跟上面那個 DCL 方法不同的是,getInstance() 方法並沒有多次去 new 對象,故不管多少個線程去調用getInstance() 方法,取的都是同一個 INSTANCE 對象,而不用去重新創建。當 getInstance() 方法被調用時,SingleTonHoler 纔在 SingleTon 的運行時常量池裏,把符號引用替換爲直接引用,這時靜態對象 INSTANCE 也真正被創建,然後再被 getInstance() 方法返回出去,這點同餓漢模式。那麼INSTANCE 在創建過程中又是如何保證線程安全的呢?在《深入理解JAVA虛擬機》中,有這麼一句話:

虛擬機會保證一個類的()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的()方法,其他線程都需要阻塞等待,直到活動線程執行()方法完畢。如果在一個類的()方法中有耗時很長的操作,就可能造成多個進程阻塞(需要注意的是,其他線程雖然會被阻塞,但如果執行()方法後,其他線程喚醒之後不會再次進入()方法。同一個加載器下,一個類型只會初始化一次。),在實際應用中,這種阻塞往往是很隱蔽的。

故而,可以看出 INSTANCE 在創建過程中是線程安全的,所以說靜態內部類形式的單例可保證線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。

那麼,是不是可以說靜態內部類單例就是最完美的單例模式了呢?其實不然,靜態內部類也有着一個致命的缺點,就是傳參的問題,由於是靜態內部類的形式去創建單例的,故外部無法傳遞參數進去,例如 Context 這種參數,所以,我們創建單例時,可以在靜態內部類與 DCL 模式裏自己斟酌。

四種線程安全的單例模式比較

  • 餓漢模式:性能好,寫法簡單,個人比較推薦用這個
  • 加synchronized關鍵字:性能差,不過對懶漢模式的蓋章比較直接有效。
  • volatile-雙重驗證加鎖:性能好,對Java版本有要求,要求Java5以上版本
  • 靜態內部類:性能好,無需加鎖,由JVM類加載保證。

🎉最後

  • 更多參考精彩博文請看這裏:《陳永佳的博客》

  • 喜歡博主的小夥伴可以加個關注、點個贊哦,持續更新嘿嘿!

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