淺談設計模式之單例模式

最近學習了Java的幾種常規的設計模式,內容較多,思維方式多種多樣,故將所學整理一下,寫成博客,分享并加深自己的理解與記憶。

首先我們看一下什么是設計模式?

設計模式(Design pattern)

設計模式(Design pattern)是一套被反復使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。使用設計模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設計模式于己于他人于系統都是多贏的;設計模式使代碼編制真正工程化;設計模式是軟件工程的基石脈絡,如同大廈的結構一樣。

設計模式分為三大類:

創建型模式

共五種:工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。

結構型模式

共七種:適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。

行為型模式

共十一種:策略模式、模板方法模式、觀察者模式、迭代子模式、責任鏈模式、命令模式、備忘錄模式、狀態模式、訪問者模式、中介者模式、解釋器模式。

其實還有兩類:并發型模式和線程池模式。

更多關于設計模式的設計原則與內容,會在陸續出完設計模式系列最后匯總一下。

接下來,進入本篇的主題——單例模式(Singleton)

單例模式(Singleton)

在我們日常的工作中經常需要在應用程序中保持一個唯一的實例,如:IO處理,數據庫操作,配置文件,工具類,線程池,緩存,日志對象等,由于這些對象都要占用重要的系統資源,所以我們必須限制這些實例的創建或始終使用一個公用的實例,如果創造出來多個實例,就會導致許多問題,比如占用過多資源,不一致的結果等。這就是我們今天要介紹的——單例模式(Singleton)。

UML

java中單例模式是一種常見的設計模式,單例模式分兩種:懶漢式單例、餓漢式單例
單例模式有以下特點:

  1. 單例類只能有一個實例。
  2. 單例類必須自己創建自己的唯一實例。
  3. 單例類必須給所有其他對象提供這一實例。

餓漢式單例(Eager initialization)

public class Singleton {
    //1.將構造方法私有化,不允許外部直接創建對象
    private Singleton(){        
    }
    
    //2.創建類的唯一實例,使用private static修飾
    private static Singleton instance=new Singleton();
    
    //3.提供一個用于獲取實例的方法,使用public static修飾
    public static Singleton getInstance(){
        return instance;
    }
}

這種方式基于classloder機制避免了多線程的同步問題,不過,instance在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance顯然沒有達到lazy loading的效果。

下面提供餓漢式的變種:

public class Singleton {  
    private Singleton instance = null;  
    static {  
        instance = new Singleton();  
    }  
    private Singleton (){}
    public static Singleton getInstance() {  
        return this.instance;  
    }  
}  

表面上看起來差別挺大,其實和之前差不多,都是在類初始化即實例化instance。

總結一下餓漢式的優點:

  • The static initializer is run when the class is initialized, after class loading but before the class is used by any thread.
  • There is no need to synchronize the getInstance() method, meaning all threads will see the same instance and no (expensive) locking is required.
  • The final keyword means that the instance cannot be redefined, ensuring that one (and only one) instance ever exists.

懶漢式單例(Lazy initialization)

/*
 * 懶漢模式
 * 區別:餓漢模式的特點是加載類時比較慢,但運行時獲取對象的速度比較快,線程安全
 *      懶漢模式的特點是加載類時比較快,但運行時獲取對象的速度比較慢,線程不安全
 */
public class Singleton {
    //1.將構造方式私有化,不允許外邊直接創建對象
    private Singleton(){
    }
    
    //2.聲明類的唯一實例,使用private static修飾
    private static Singleton instance;
    
    //3.提供一個用于獲取實例的方法,使用public static修飾
    public static Singleton getInstance(){
        if(instance==null){
            instance=new Singleton();
        }
        return instance;
    }
}

這種寫法lazy loading很明顯,但是致命的是在多線程不能正常工作。
其實懶漢式也是可以寫成線程安全的,代碼如下:

public class Singleton {
    private static volatile Singleton instance;
    private Singleton() { }

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

這種寫法能夠在多線程中很好的工作,而且看起來它也具備很好的lazy loading,但是,遺憾的是,效率很低,99%情況下不需要同步。

寫博客的時候,發現了WIKI提供了一個更加牛的懶漢式升級版——雙重校驗鎖

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}   
    public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}  

雙重校驗鎖(Double-checked locking),俗稱雙重檢查鎖定。

其他單例的實現

登記式單例

//類似Spring里面的方法,將類名注冊,下次從里面直接獲取。
public class Singleton3 {
    private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();
    static{
        Singleton3 single = new Singleton3();
        map.put(single.getClass().getName(), single);
    }
    //保護的默認構造子
    protected Singleton3(){}
    //靜態工廠方法,返還此類惟一的實例
    public static Singleton3 getInstance(String name) {
        if(name == null) {
            name = Singleton3.class.getName();
            System.out.println("name == null"+"--->name="+name);
        }
        if(map.get(name) == null) {
            try {
                map.put(name, (Singleton3) Class.forName(name).newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return map.get(name);
    }
    //一個示意性的商業方法
    public String about() {    
        return "Hello, I am RegSingleton.";    
    }    
    public static void main(String[] args) {
        Singleton3 single3 = Singleton3.getInstance(null);
        System.out.println(single3.about());
    }
}

登記式單例實際上維護了一組單例類的實例,將這些實例存放在一個Map(登記薄)中,對于已經登記過的實例,則從Map直接返回,對于沒有登記的,則先登記,然后返回。

枚舉(The enum way)

public enum Singleton {
    INSTANCE;
    public void execute (String arg) {
        // Perform operation here 
    }
}

居然用枚舉!!看上去好牛逼,通過Singleton.INSTANCE來訪問,這比調用getInstance()方法簡單多了。這種方式是《Effective Java》作者Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,可謂是很堅強的壁壘啊。

默認枚舉實例的創建是線程安全的,所以不需要擔心線程安全的問題。但是在枚舉中的其他任何方法的線程安全由程序員自己負責。還有防止上面的通過反射機制調用私用構造器。

這個版本基本上消除了絕大多數的問題。代碼也非常簡單,實在無法不用。

靜態內部類(Static block initialization)

public class Singleton {
    private static final Singleton instance;

    static {
        try {
            instance = new Singleton();
        } catch (Exception e) {
            throw new RuntimeException("Darn, an error occurred!", e);
        }
    }

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {
        // ...
    }
}

這種方式同樣利用了classloder的機制來保證初始化instance時只有一個線程,它跟餓漢式不同的是(很細微的差別):餓漢式是只要Singleton類被裝載了,那么instance就會被實例化(沒有達到lazy loading效果),而這種方式是Singleton類被裝載了,instance不一定被初始化。因為SingletonHolder類沒有被主動使用,只有顯示通過調用getInstance方法時,才會顯示裝載SingletonHolder類,從而實例化instance。想象一下,如果實例化instance很消耗資源,我想讓他延遲加載,另外一方面,我不希望在Singleton類加載時就實例化,因為我不能確保Singleton類還可能在其他的地方被主動使用從而被加載,那么這個時候實例化instance顯然是不合適的。這個時候,這種方式相比餓漢式就顯得很合理。

餓漢式和懶漢式的區別

這兩種乍看上去非常相似,其實是有區別的,主要兩點

1、線程安全:

餓漢式是線程安全的,可以直接用于多線程而不會出現問題,懶漢式就不行,它是線程不安全的,如果用于多線程可能會被實例化多次,失去單例的作用。

如果要把懶漢式用于多線程,有兩種方式保證安全性,一種是在getInstance方法上加同步,另一種是在使用該單例方法前后加雙鎖。

2、資源加載:

餓漢式在類創建的同時就實例化一個靜態對象出來,不管之后會不會使用這個單例,會占據一定的內存,相應的在調用時速度也會更快,

而懶漢式顧名思義,會延遲加載,在第一次使用該單例的時候才會實例化對象出來,第一次掉用時要初始化,如果要做的工作比較多,性能上會有些延遲,之后就和餓漢式一樣了。

什么是線程安全?

如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

或者說:一個類或者程序所提供的接口對于線程來說是原子操作,或者多個線程之間的切換不會導致該接口的執行結果存在二義性,也就是說我們不用考慮同步的問題,那就是線程安全的。

應用

以下是一個單例類使用的例子,以懶漢式為例

public class TestSingleton {
    String name = null;

        private TestSingleton() {
    }

    private static TestSingleton ts = null;

    public static TestSingleton getInstance() {
        if (ts == null) {
            ts = new TestSingleton();
        }
        return ts;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void printInfo() {
        System.out.println("the name is " + name);
    }

}
public class TMain {
    public static void main(String[] args){
        TestStream ts1 = TestSingleton.getInstance();
        ts1.setName("jason");
        TestStream ts2 = TestSingleton.getInstance();
        ts2.setName("0539");
        
        ts1.printInfo();
        ts2.printInfo();
        
        if(ts1 == ts2){
            System.out.println("創建的是同一個實例");
        }else{
            System.out.println("創建的不是同一個實例");
        }
    }
}

運行結果:


result

結論:由結果可以得知單例模式為一個面向對象的應用程序提供了對象惟一的訪問點,不管它實現何種功能,整個應用程序都會同享一個實例對象。

參考資料:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容