天天看點

設計模式:單例的n種寫法

寫法(1):餓漢式

public class Singleton1 {

    private static final Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {}

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

}
           

我們通過new 構造方法來建立對象,為了實作一個類是單例的,隻有一個執行個體對象,自然就不允許随意的調用構造函數了,是以對于單例類,要将構造方法私有化(private),這樣就無法在外部調用構造方法來建立對象了。然後提供一個靜态方法對外暴露對象,為什麼要是靜态方法呢?因為調用靜态方法無需建立對象,通過類本身就能完成調用了。

測試:

public class Main {

    public static void main(String[] args) {
        for (int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Singleton1.getInstance().toString());
            }).start();
        }
    }
}
           

從控制台輸出結果可以看見,在多線程環境下擷取到的都是同一個對象,此種實作方式是線程安全的。因為JVM可以保證靜态變量隻會被初始化一次。

設計模式:單例的n種寫法

為什麼上面的寫法稱為餓漢式呢,因為執行個體對象INSTANCE會在Singleton1類進行初始化的時候就會被建立出來。此時不管有沒有調用getInstance方法來擷取對象,對象都已經被建立了。

寫法(2):懶漢式-非線程安全版本

懶漢式與餓漢式的差別就是,在第一次調用getInstance的時候才會進行執行個體對象的建立。若一直沒有調用getInstance方法,則對象始終不會被建立。

public class Singleton2 {

    private static Singleton2 INSTANCE = null;

    private Singleton2() {}

    public static Singleton2 getInstance() {
        if (INSTANCE == null) {
            // (1)
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }

}
           

此種方式不是線程安全的。在多線程調用下,會出現建立多個對象的情況。比如,當線程A調用getInstance方法,代碼執行到(1)的位置,此時CPU執行線程A的時間片結束,線程A暫停等待。CPU開始執行線程B,線程B調用getInstance,此時的INSTANCE執行個體還沒有建立,線程B建立對象并傳回。緊接着線程A從暫停的地方繼續執行,也會建立對象傳回。于是就會出現建立多個對象的情況。

測試:

修改getInstance方法,讓執行邏輯暫停1秒,模拟CPU暫停排程。

public static Singleton2 getInstance() {
    if (INSTANCE == null) {
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        INSTANCE = new Singleton2();
    }
    return INSTANCE;
}
           

運作測試方法:

public class Main {

    public static void main(String[] args) {
        for (int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Singleton2.getInstance().toString());
            }).start();
        }
    }
}
           

從控制台輸出結果可以看出,多線程調用下确确實實會出現建立多個對象的情況,這就不滿足單例了。但是如果你隻是在單線程下使用,這種方式是完全沒有問題的。

設計模式:單例的n種寫法

寫法(3):線程安全版本synchronized方法

對于實作方式(2)中存線上程安全的問題,那就來解決線程安全問題啊。Java為我們提供了多種保證線程安全的實作方式,比如使用synchronized,或者使用ReentrantLock等。在這裡隻介紹使用synchronized實作線程安全。

public class Singleton3 {

    private static Singleton3 INSTANCE = null;

    private Singleton3() {}

    public static synchronized Singleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

}
           

對方法getInstance使用了synchronized,保證了多線程調用該方法的線程安全。

測試:

public static synchronized Singleton3 getInstance() {
    if (INSTANCE == null) {
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        INSTANCE = new Singleton3();
    }
    return INSTANCE;
}
           
public class Main {

    public static void main(String[] args) {
        for (int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Singleton3.getInstance().toString());
            }).start();
        }
    }
}
           

保證了線程安全,同時保證了單例。

設計模式:單例的n種寫法

此種實作方式雖然能夠保證線程安全,但是在方法上使用synchronized,此時synchronized鎖住的是目前類的Class對象。此時若該方法中還存在一個synchronized修飾的方法,當一個線程調用getInstance方法時,其他線程調用另一個同步方法也會出現鎖競争的問題,可能會導緻線程阻塞等待。還有就是每次調用getInstance方法都要加鎖解鎖,是不是很麻煩。

寫法(4):線程安全版本雙重檢查

既然synchronized直接修飾方法不完美,那使用synchronized修飾代碼塊呢。此種實作方式就是雙重檢查(Double-Check)

public class Singleton4 {
    // 變量需要使用volatile,避免指令重排
    private static volatile Singleton4 INSTANCE = null;

    private static Object object = new Object();

    private Singleton4() {}

    public static Singleton4 getInstance() {
        // (1)
        if (INSTANCE == null) {
            // (2)
            synchronized (object) {
                // (3)
                if (INSTANCE == null) {
                    INSTANCE = new Singleton4();
                }
            }
        }
        return INSTANCE;
    }

}
           

之是以叫雙重檢查,是因為會在(1)與(3)兩處進行是否為空判斷。為什麼要進行兩次檢查呢,一次不行嗎?對于(1)處的為空判斷,若此時INSTANCE不為空,即對象已經建立過了,此時可以直接傳回已經建立的對象。此時還不會觸發競争鎖的操作。另一種情況,若線程A在一處進行判斷是否為空,此時INSTANCE為空,繼續執行下面的代碼。此時會去擷取object對象鎖,若線程A順利擷取到鎖,進入synchronized代碼塊。此時線程B進入,此時若INSTANCE還為空,線程B要等待線程A執行完成釋放鎖,才能擷取鎖,執行代碼塊。想想鎖沒有(3)處的非空判斷,線程A執行完代碼塊建立一個對象了,将INSTANCE引用指向建立的對象。線程B進入代碼塊,沒有進行非空判斷,又會執行new 建立對象。這樣又會出現建立多個對象的問題。是以需要進行雙重檢查,第一次檢查是為了避免競争鎖進而提高效率,第二次檢查能夠避免重複建立對象。

另外變量INSTANCE需要使用volatile修飾,來避免指令重排。指令重排出現的機率很低,可能幾千萬次調用中會出現幾次。

測試:

public static Singleton4 getInstance() {
    if (INSTANCE == null) {
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        synchronized (object) {
            if (INSTANCE == null) {
                INSTANCE = new Singleton4();
            }
        }
    }
    return INSTANCE;
}
           
public class Main {

    public static void main(String[] args) {
        for (int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Singleton4.getInstance().toString());
            }).start();
        }
    }
}
           
設計模式:單例的n種寫法

寫法(5):靜态内部類

public class Singleton5 {

    private Singleton5() {}

    public static Singleton5 getInstance() {
        return InnerClass.get();
    }

    private static class InnerClass {
        
        private static final Singleton5 singleton5 = new Singleton5();

        public static Singleton5 get() {
            return singleton5;
        }

    }

}
           

内部類也是可以實作懶加載餓漢式的單例的。對于靜态内部類InnerClass隻有在第一次調用使用的時候才會進行初始化與建立對象。是以隻有第一次調用了getInstance方法時,才會觸發調用InnerClass.get(),才會進行singleton5對象的建立。

寫法(6):枚舉

public enum Singleton6 {

    INSTANCE;
    
}
           

枚舉天然實作了單例。可以在枚舉中定義執行個體方法,實作單例的調用。

public enum Singleton6 {

    INSTANCE;

    public void sayHello() {
        System.out.println("hello world");
    }

}
           
public class Main {

    public static void main(String[] args) {
        Singleton6.INSTANCE.sayHello();
    }
}
           

繼續閱讀