單例模式——我只有一個對象

面試官:帶筆了吧,那寫兩種單例模式的實現方法吧

沙沙沙刷刷刷~~~ 寫好了

面試官:你這個是怎麼保證線程安全的,那你知道,volatile 關鍵字? 類加載器?鎖機制????
點贊+收藏 就學會系列,文章收錄在 GitHub JavaEgg ,N線互聯網開發必備技能兵器譜

單例模式——獨一無二的對象

單例模式,從我看 《Java 10分鐘入門》那天就聽過的一個設計模式,還被面試過好幾次的設計模式問題,今天一網打盡~~

有一些對象我們確實只需要一個,比如,線程池、數據庫連接、緩存、日誌對象等,如果有多個的話,會造成程序的行爲異常,資源使用過量或者不一致的問題。你也許會說,這種我用全局變量不也能實現嗎,還整個單例模式,好像你很流弊的樣子,如果將對象賦值給一個全局變量,那程序啓動就會創建好對象,萬一這個對象很耗資源,我們還可能在某些時候用不到,這就造成了資源的浪費,不合理,所以就有了單例模式。

單例模式的定義

單例模式確保一個類只有一個實例,並提供一個全局唯一訪問點

單例模式的類圖

單例模式的實現

餓漢式

  • static 變量在類裝載的時候進行初始化
  • 多個實例的 static 變量會共享同一塊內存區域

用這兩個知識點寫出的單例類就是餓漢式了,初始化類的時候就創建,飢不擇食,餓漢

public class Singleton {

    //構造私有化,防止直接new
    private Singleton(){}

    //靜態初始化器(static initializer)中創建實例,保證線程安全
    private static Singleton instance = new Singleton();

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

餓漢式是線程安全的,JVM在加載類時馬上創建唯一的實例對象,且只會裝載一次。

Java 實現的單例是一個虛擬機的範圍,因爲裝載類的功能是虛擬機的,所以一個虛擬機通過自己的ClassLoader 裝載餓漢式實現單例類的時候就會創建一個類實例。(如果一個虛擬機裏有多個ClassLoader的話,就會有多個實例)

懶漢式

懶漢式,就是實例在用到的時候纔去創建,比較“懶”

單例模式的懶漢式實現方式體現了延遲加載的思想(延遲加載也稱懶加載Lazy Load,就是一開始不要加載資源或數據,等到要使用的時候才加載)

同步方法

public class Singleton {
    private static Singleton singleton;

    private Singleton(){}

  	//解決了線程不安全問題,但是效率太低了,每個線程想獲得類的實例的時候,都需要同步方法,不推薦
    public static synchronized Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

雙重檢查加鎖

public class Singleton {

  	//volatitle關鍵詞確保,多線程正確處理singleton
    private static volatile Singleton singleton;
  
    private Singleton(){}
  
    public static Singleton getInstance(){
        if(singleton ==null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

Double-Check 概念(進行兩次檢查)是多線程開發中經常使用的,爲什麼需要雙重檢查鎖呢?因爲第一次檢查是確保之前是一個空對象,而非空對象就不需要同步了,空對象的線程然後進入同步代碼塊,如果不加第二次空對象檢查,兩個線程同時獲取同步代碼塊,一個線程進入同步代碼塊,另一個線程就會等待,而這兩個線程就會創建兩個實例化對象,所以需要在線程進入同步代碼塊後再次進行空對象檢查,才能確保只創建一個實例化對象。

雙重檢查加鎖(double checked locking)線程安全、延遲加載、效率比較高

volatile:volatile一般用於多線程的可見性,這裏用來防止指令重排(防止new Singleton時指令重排序導致其他線程獲取到未初始化完的對象)。被volatile 修飾的變量的值,將不會被本地線程緩存,所有對該變量的讀寫都是直接操作共享內存,從而確保多個線程能正確的處理該變量。

指令重排

指令重排是指在程序執行過程中, 爲了性能考慮, 編譯器和CPU可能會對指令重新排序。

Java中創建一個對象,往往包含三個過程。對於singleton = new Singleton(),這不是一個原子操作,在 JVM 中包含如下三個過程。

  1. 給 singleton 分配內存

  2. 調用 Singleton 的構造函數來初始化成員變量,形成實例

  3. 將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null 了)

但是,由於JVM會進行指令重排序,所以上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3,也可能是 1-3-2。如果是 1-3-2,則在 3 執行完畢,2 未執行之前,被另一個線程搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以這個線程會直接返回 instance,然後使用,那肯定就會報錯了,所以要加入 volatile關鍵字。

靜態內部類

public class Singleton {

    private Singleton(){}

    private static class SingletonInstance{
        private static final Singleton INSTANCE = new Singleton();
    }
  
    public static Singleton getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

採用類加載的機制來保證初始化實例時只有一個線程;

靜態內部類方式在Singleton 類被裝載的時候並不會立即實例化,而是在調用getInstance的時候,纔去裝載內部類SingletonInstance ,從而完成Singleton的實例化

類的靜態屬性只會在第一次加載類的時候初始化,所以,JVM幫我們保證了線程的安全性,在類初始化時,其他線程無法進入

優點:線程安全,利用靜態內部類實現延遲加載,效率較高,推薦使用

枚舉

enum Singleton{
  INSTANCE;
  public void method(){}
}

藉助JDK5 添加的枚舉實現單例,不僅可以避免多線程同步問題,還能防止反序列化重新創建新的對象,但是在枚舉中的其他任何方法的線程安全由程序員自己負責。還有防止上面的通過反射機制調用私用構造器。不過,由於Java1.5中才加入enum特性,所以使用的人並不多。

這種方式是《Effective Java》 作者Josh Bloch 提倡的方式。

單例模式在JDK 中的源碼分析

JDK 中,java.lang.Runtime 就是經典的單例模式(餓漢式)

單例模式注意事項和細節

  • 單例模式保證了系統內存中該類只存在一個對象,節省了系統資源,對於一些需要頻繁創建銷燬的對象,使用單例模式可以提高系統性能
  • 當想實例化一個單例類的時候,必須要記住使用相應的獲取對象的方法,而不是使 用new
  • 單例模式使用的場景:需要頻繁的進行創建和銷燬的對象、創建對象時耗時過多或 耗費資源過多(即:重量級對象),但又經常用到的對象、工具類對象、頻繁訪問數 據庫或文件的對象(比如數據源、session工廠等)

設計模式前傳——學習設計模式前要知道的

發佈了68 篇原創文章 · 獲贊 120 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章