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

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

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

面試官:你這個是怎麼保證線程安全的,那你知道,volatile 關鍵字? 類加載器?鎖機制????git

點贊+收藏 就學會系列,文章收錄在 GitHub JavaEgg ,N線互聯網開發必備技能兵器譜程序員

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

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

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

單例模式的定義

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

單例模式的類圖

單例模式的實現

餓漢式

  • 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的話,就會有多個實例)安全

懶漢式

懶漢式,就是實例在用到的時候纔去建立,比較「懶」session

單例模式的懶漢式實現方式體現了延遲加載的思想(延遲加載也稱懶加載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工廠等)

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

相關文章
相關標籤/搜索