天天看點

設計模式之單例模式

聊聊單例模式,面試加分題

猶記得之前面xx時,面試官一上來就問你知道哪些設計模式,來手寫一個單例模式的場景;尴尬的我,隻寫了懶漢式餓漢式,對于單例其他的變種一概不知;這次就來彌補下這方面的知識盲區!

餓漢式

餓漢式,從字面上了解就是很餓,一上來就要吃的,那麼它會把吃的先準備好,以滿足它的需求;那麼對應到程式上的表現就為:在類加載的時候就會首先進行執行個體的初始化,後面如果應用程式需要這個執行個體的話,就有現成的了,可以直接使用目前的單例對象!

我們來手寫下餓漢式的代碼:

public class Singleton{
    // 聲明靜态私有執行個體 并執行個體化
    private static Singleton singleton = new Singleton();

    // 提供對外初始化方法 靜态類加載就初始化
    public static Singleton initInstance(){
        return singleton;
    }

    // 聲明私有構造方法  即在外部類無法通過new 初始化執行個體
    private Singleton(){

    }

    public void doSomeThing(){
        System.out.println("do some thing!");
    }
}
class SingletonDemo{
    public static void main(String[] args) {
        Singleton singleton = Singleton.initInstance();
    }
}
           

餓漢式的優點:它是線程安全的,因為單例對象在類加載的時候就被初始化了,當調用單例對象時隻需要去把對應的對象指派給變量即可!

餓漢式的缺點:如果這個類不經常使用,會造成一定的資源浪費!

懶漢式

懶漢式,就是比較懶,每次需要填飽肚子時才會外出覓食;那麼對應到程式層面的了解:當應用程式需要某個對象時,該對象的類就會去建立一個執行個體,而不是提前準備好的!

我們來手寫下懶漢式的代碼:

public class Singleton2 {
    // 聲明私有靜态對象
    private static Singleton2 singleton2;

    // 對外提供初始化方法
    public static Singleton2 initInstance(){
        if(singleton2 == null){
            singleton2 = new Singleton2();
        }
        return singleton2;
    }

    // 私有構造器
    private Singleton2(){

    }

    public void doSomeThing(){
        System.out.println("do some thing!");
    }
}
class SingletonDemo2{
    public static void main(String[] args) {
        Singleton2 singleton2 = Singleton2.initInstance();
        singleton2.doSomeThing();
    }
}
           

同樣我們看下懶漢式的優點:不會造成資源的浪費

懶漢式的缺點:多線程情況下,會有線程安全的問題;

上面我們可以看到,餓漢式和懶漢式的唯一差別就是:餓漢式在類加載時就完成了對象的初始化,而懶漢式是在需要初始化的時候再去初始化對象;其實在單線程情況下,他們都是線程安全的;但是我們寫的代碼,必須考慮多線程情況下的并發問題,那麼懶漢式的這種寫法基本不滿足需求,我們需要做些改造,使得它變得線程安全,滿足我們的需求!

雙重檢測鎖

我們知道,懶漢式下對象的初始化在并發環境下,可能多個線程同時執行到

singleton2 == null

,進而初始化了多個執行個體,這就引發了線程安全問題!

我們就需要改寫它的初始化方法,我們知道加鎖可以解決一般的線程安全問題,

synchronized

這個關鍵字可以修飾一個代碼塊或方法,被其修飾的方法或代碼塊就被加了鎖;而從某些方面了解,

synchronized

是個同步鎖,亦是個可重入鎖!哈哈,關于鎖的種類及概念有點多,後面準備寫一篇關于鎖的部落格來總結下;不再發散了,回歸正題

我們來改造下懶漢式的初始化方法如下:

// 對外提供初始化方法
public synchronized static Singleton2 initInstance(){
    if(singleton2 == null){
        singleton2 = new Singleton2();
    }
    return singleton2;
}
           

我們看下上面的代碼,初看沒什麼問題是解決了線程安全問題;但是由于整個方法都被

synchronized

修飾,那麼在多線程的情況下就增加了線程同步的開銷,降低了程式的執行效率;為了改進這個問題,我們将

synchronized

放入到方法内,實作代碼塊的同步;改下如下:

// 對外提供初始化方法
public  static Singleton2 initInstance(){
    if(singleton2 == null){
        synchronized(Singleton2.class){
            singleton2 = new Singleton2();
        }
    }
    return singleton2;
}
           

呃,這樣就滿足了我們的要求了嗎?聰明如你一定發現了,雖然我們将

synchronized

移到了方法内部,降低了同步的開銷,但是在并發的情況下假設多個線程同時執行到

if(singleton2 == null)

時,依舊會排隊初始化

Singleton2

執行個體,這樣又會造成新的線程安全問題;那麼為了解決這個問題,就出現了大名鼎鼎的“雙重檢測鎖”。我們來看下它的實作,将上述代碼改寫如下:

// 對外提供初始化方法
public  static Singleton2 initInstance(){
    if(singleton2 == null){// 第一次非空判斷
        synchronized(Singleton2.class){
            if(singleton2 == null)// 第二次非空判斷
                singleton2 = new Singleton2();
        }
    }
    return singleton2;
}
           

哈哈,這個雙重即是判斷兩次的意思,并不是加兩把鎖哈;那麼這樣就能行了嗎?初看沒問題啊,但是我們細想之下這樣寫真的沒問題嗎?你寫的代碼,執行的時候真的會按你想的過程執行嗎?有沒有考慮過指令重排呢?問題就出現在

new Singleton2()

這個代碼上,這行代碼不是一個原子操作!

我們再來回顧下指令重排的大緻執行流程:

1.給對象執行個體配置設定記憶體空間

2.調用對象構造方法,初始化成員變量

3.将構造的對象指向配置設定的記憶體空間

問題就出在指令重排後,cpu對指令重排的優化上,也就是說上述的三個過程并不是每次都是1-2-3順序執行的,而是也有可能1-3-2;那麼我們試想下并發情況下可能出現的場景,當線程A執行到步驟3時,cpu時間片正好輪詢到線程B,那麼線程B判斷執行個體已經指向了對應的記憶體空間,不為null就不會 初始化執行個體了,就得到了一個未初始化完成的對象,這就導緻了問題的誕生!

為了解決這個問題,我們知道還有一個關鍵字

volatile

可以完美的解決指令重排,使得非原子性的操作對其他對象是可見的!(volatile關鍵字保障了變量的記憶體的可見性和一緻性問題,關于記憶體屏障可以看我之前的一篇文章JMM 記憶體模型知識點探究了解)。那麼我們将懶漢式改寫如下:

public class Singleton2 {
    // 聲明私有靜态對象
    private volatile static Singleton2 singleton2;

    // 對外提供初始化方法
    public  static Singleton2 initInstance(){
        if(singleton2 == null){
            synchronized(Singleton2.class){
                if(singleton2 == null)
                    singleton2 = new Singleton2();
            }
        }
        return singleton2;
    }

    // 私有構造器
    private Singleton2(){

    }

    public void doSomeThing(){
        System.out.println("do some thing!");
    }
}
class SingletonDemo2{
    public static void main(String[] args) {
        Singleton2 singleton2 = Singleton2.initInstance();
        singleton2.doSomeThing();
    }
}
           

其實除了上面的單例實作外,還有兩種常見的單例實作

靜态内部類

代碼如下:

public class InnerClassSingleton {
    // 私有靜态内部類
    private static class InnerInstance{
        private static final InnerClassSingleton singleton = new InnerClassSingleton();
    }
    // 對外提供的初始化方法
    public static InnerClassSingleton initInstance(){
        return InnerInstance.singleton;
    }
    // 私有構造器
    private InnerClassSingleton(){

    }

    public void doSomeThing(){
        System.out.println("do some thing!");
    }
}
class InnerClassSingletonDemo{
    public static void main(String[] args) {
        InnerClassSingleton innerClassSingleton = InnerClassSingleton.initInstance();
        innerClassSingleton.doSomeThing();
    }
}
           

其實,靜态内部類的方式和餓漢式本質是一樣的,都是根據類加載機制來初始化執行個體,進而保證單例和線程安全的;不同的是靜态内部類的方式是按需建構執行個體,不會如餓漢式一樣造成資源浪費的問題;是以這個是餓漢式一個比較好的變種!

枚舉類

枚舉是比較推薦的一種單例模式,它是線程安全的,且通過反射、序列化以及反序列化都無法破壞它的單例屬性(其他的單例采用私有構造器的實作其實并不安全),至于為什麼呢?這個可以參考部落格:[為什麼要用枚舉實作單例模式(避免反射、序列化問題)]

public class EnumSingleton {
    // 聲明私有的枚舉類型
    private enum Enum{
        INSTANCE;
        // 聲明單例對象
        private final EnumSingleton instance;
        // 執行個體化
        Enum(){
            instance = new EnumSingleton();
        }
        private EnumSingleton getInstance(){
            return instance;
        }
    }
    // 對外提供的初始化方法
    public static EnumSingleton initInstance(){
        return Enum.INSTANCE.getInstance();
    }

    // 私有構造器
    private EnumSingleton(){

    }

    public void doSomeThing(){
        System.out.println("do some thing!");
    }
}
class EnumSingletonDemo{
    public static void main(String[] args) {
        EnumSingleton enumSingleton = EnumSingleton.initInstance();
        enumSingleton.doSomeThing();
    }
}
           
好,至此我們總結了單例的幾種實作方式;比較推薦的是後面兩種方式,一般懶漢式我們就采用雙重檢測鎖的方式;你可以發散思考下單例的應用場景,例如Spring中的Bean的初始化就是單例模式的典型應用,或者在消息中心中使用比較頻繁的短連結!

餘路那麼長,還是得帶着虔誠上路...

繼續閱讀