單例模式(singleton)之“世上安得雙全法”


返滬隔離在住處,遠程辦公悶得慌,寫篇水文來湊數~_^


單例模式作為設計模式的入門模式,網上有各種寫法,有點象孔乙己“茴”字的四種寫法,都研究爛了,還能玩出啥新意?稍安勿躁,先來回顧一下:

 

一、餓漢式

/**
 * 餓漢式
 */
public class Single01 {

    private Single01() {

    }

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

    private static Single01 instance = new Single01();

    public static Single01 getInstance() {
        return instance;
    }

}

從類加載的機制可以知道,這種寫法,一旦classloader加載后,instance靜態變量就被實例化了,不管你用不用得到。猶如餓了三天的漢子,見到食物就狼吞虎咽,不管好不好吃,有沒有毒,由此得名。

 

二、懶漢式

既然“餓漢式”式寫法,吃相難看,於是大佬們又研究出了下面的寫法:(這里我們只說線程安全的寫法,非線程安全的不提也罷)

package singleton;

public class Single02 extends SuperClass {

    private Single02() {
    }

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

    private static volatile Single02 instance = null;

    public static Single02 getInstance() {
        if (instance == null) {
            synchronized (Single02.class) {
                if (instance == null) {
                    instance = new Single02();
                }
            }
        }
        return instance;
    }
}

大意是:如果用不到,就不實例化,classLoader裝載時,instance為null,僅在第1次調用getInstance時才new對象。好比一個懶漢,非到餓得不行了,才去弄吃的,故名:懶漢式。

缺點:太復雜了,有點秀!這個雙重檢測(double check)以及volatile的作用,對於初學者得琢磨半天。

 

三、金屋藏嬌式

package singleton;

public class Single03 {

    private Single03() {
    }

    private static class InnerHolder {
        private static Single03 instance = new Single03();
    }

    public static Single03 getInstance() {
        return InnerHolder.instance;
    }

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

鑒於懶漢式的寫法太過復雜,於是又有人想到了:借助一個內部靜態類,把需要的實例先偷偷藏起來,等到要用時才請出來,是為“金屋藏嬌”。這個寫法,個人認為算是常規寫法中最好的1個。

 

四、固若金湯法(enum法)

前3種寫法都有一個致命缺點,無法抵擋反序列化搗亂。試想“單例”的初衷,就是保證同一個jvm中不能new出2個相同的實例,必須“天下無雙”。可惜事與願違,java創建實例的方法不僅僅只有構造函數new這一種,可以把現有實例序列化成字符串(比如:json序列化),然后再拿json串反序列化成新對象,相當於人類的生物clone技術,雖然克隆出來的兄弟,長相不分你我,但我們都知道“好看的皮囊千篇一律,有趣的靈魂獨各不相同”。所以《effective java》中提出一種新方法:

package singleton;

public enum Single04 {

    INSTANCE;

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


}

這個寫法可謂思路清奇,java中的enum本身也是一個類(雖然有點特殊),但是jvm規定enum沒有構造函數,而且內部就是靜態類,所以天然單例,關鍵還能防止反序列化攻擊,比如下面的代碼:

Gson gson = new Gson();
Single04 single04a = Single04.INSTANCE;
String s04 = gson.toJson(single04a);
System.out.println(s04);
Single04 single04b = gson.fromJson(s04, Single04.class);
single04b.sayHello();
System.out.println(single04a.hashCode() + " " + single04b.hashCode());

輸出:

"INSTANCE"
hello 4
2051450519 2051450519

看第3行,2個實例的hashcode完全相同,說明就是同1個對象。而上述測試代碼,換成前3種寫法的任何1種:

Gson gson = new Gson();
Single03 single03a = Single03.getInstance();
String s03 = gson.toJson(single03a);
System.out.println(s03);
Single03 single03b = gson.fromJson(s03, Single03.class);
single03b.sayHello();
System.out.println(single03a.hashCode() + " " + single03b.hashCode());

輸出:

{}
hello 3
1450821318 668849042

第3行看出,這2個實例的hashcode已經不同了,說明是2個不同的實例。

所以,從安全角度來看,enum用作單例毫無破綻,稱之為“固若金湯法”名副其實!

 

等等!這就天下太平,人生圓滿了嗎?OO的世界中,還有多態呢! 如果這個單例類,需要繼承自父類怎么弄?

終於,生活還是對我們下了狠手,人生太艱難了!enum不允許繼承父類!!!

 

正所謂

世間安得雙全法,不負如來不負卿

既然如此,那就... 洗洗睡吧,夢里什么都有!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM