深入靈魂的考驗,每行註釋都是靈魂的單例模式,源碼+實例降臨

不管是設計模式也好,別的模式也要,他都是爲了解決問題而發明的有效的方法。除了我們已經熟悉的23種設計模式以外,還有MVVMCombinator等其它的東西,都已經是前輩們經過多年的摸爬滾打總結出來的,其有效性不容置疑。我這篇文章也不會用來證明設計模式是有用的,因爲在我看來,這就跟1+1=2一樣明顯(在黑板上寫下1+1=2)

而這,在現在這個追求高質量代碼的時代,雖然顯得有一些複雜,但是我個人還是“推崇”(看好了,我有引號的)這個東西,畢竟面試必問系列,你咋整

來看今天的內容吧,有代碼,有實例,並且有一些內容我直接放在代碼中通過註釋進行講解,會更好理解

一、設計部分:單例的實現思想、代碼及注意問題

packagecom.test.hibernate;/*生成一個懶漢式單例的基礎理解:

1.Singleton顧名思義就是隻能創建一個實例對象。。所以不能擁有public的構造方法

2.既然構造方法是私有的,那麼從外面不可能創建Singleton實例了。。只能從內部創建。。所以需要一個方法來創建此實例,也因此只能通過類名來創建對象。。此方法肯定必須是static的

3.靜態的getInstance方法要返回一個Singleton實例。。就要一個Singleton類型的變量來存儲。。聲明一個Singleton類型的屬性。。同樣需要是static 的。。靜態方法只能訪問靜態屬性。。。

!!!前3步是單例的共性!後三步是懶漢式需要考慮的地方!

4.爲了保證只生成一個實例,需要做判斷是否爲null

5.此時考慮線程問題,假設有兩個線程。。thread1,thread2。。thread1運行到判斷那個Singleton類型的變量是否爲null,然後跳到了thread2。。也運行到判斷之後。。。此時兩線程都得到single爲空。。。那麼就會有兩個實例了。。。解決辦法。。同步

6.同步又要考慮效率,不能有太多的沒用同步

* *///思考總結:其實餓漢式沒什麼問題,問題就是出現在懶漢式上,一般就是牽扯到執行效率和線程安全2個角度上來思考//個人感覺 如果是餓漢式就用天然的沒毛病,如果想用懶漢式就用靜態內部類方式吧//存在問題:如何在2個jvm上保證單例還未解決:這個就牽扯到分佈式鎖,可以用zookeeper來實現//還缺少一種懶漢式的枚舉方式實現有待研究,聽說這個方法也不錯。publicclassdanli{//模擬一下靜態代碼塊的使用方式,靜態代碼塊在類加載的運行,先靜態代碼塊》再構造代碼塊》再構造函數 ,只研究單例可以忽略publicstaticfinalString STR1;static{        STR1 =newString("zzh");    }}//下面正式演示各種單例的實現:classdanli2{//單例餓漢式(非延時加載),提前加載,有利於速度和反應時間,天然的線程安全的。沒毛病privatedanli2(){};privatestaticfinaldanli2 two =newdanli2();//final可加可不加,final的目的就是最終的,只允許一次賦值,但不加是因爲沒法在本類外給他賦值了,因爲構造方法是私有的沒法創建這個類的對象了,而且這個成員變量也是私有的所以不能在外面調用到,但是可以在本類中的其他方法調用到,所以其實還是可以修改的,所以還是加上final吧publicstaticdanli2getSingleInstance(){returntwo;    }}classdanli3{//單例懶漢式(延時加載),用的時候再去加載,有利於資源充分利用privatedanli3(){};privatestaticdanli3 three =null;publicstaticsynchronizeddanli3getSingleInstance(){//加上synchronized變得線程安全了,但是效率下降了,每次還需要檢查同步等等if(three ==null){//保證只生成一個實例three =newdanli3();        }returnthree;    }}/* 該類跟上面那個是一樣的,上面是synchronized方法,下面這個是代碼塊。

class Singleton {

private Singleton() {}

private volatile static Singleton instance = null;

public static Singleton getInstance() {

  synchronized (Singleton.class) {//利用synchronized代碼塊,每次需要先檢查有沒有同步鎖,效率較低,爲了解決這個問題又提出了加入雙層檢查,也就是在這個同步代碼塊的外面再加一層爲null判斷,來減少除第一次以外的同步檢查,提高了效率

    if (instance == null) {

    instance = new Singleton();

    }

  }

  return instance;

}

}

*/雙重檢查加鎖就是在同步代碼塊的外面一層再來一個==null的判斷,解決除第一次以外所有的同步判斷導致的效率下降問題//但是這個雙重檢查加鎖在多線程環境下存在系統崩潰的可能(一個線程初始化一半的對象,被第二個線程直接拿去用了,所以系統崩潰了)/*原因如下

1、線程 1 進入 getInSingleton() 方法。

2、由於 uniqueInstance 爲 null,線程 1 在 //1 處進入 synchronized 塊。

3、線程 1 前進到 //3 處,但在構造函數執行之前,使實例成爲非 null。

4、線程 1 被線程 2 預佔。

5、線程 2 檢查實例是否爲 null。因爲實例不爲 null,線程 2 將 uniqueInstance 引用返回給一個構造完整但部分初始化了的 Singleton 對象。

6、線程 2 被線程 1 預佔。

7、線程 1 通過運行 Singleton 對象的構造函數並將引用返回給它,來完成對該對象的初始化。

*/classSingleton{//雙重檢查加鎖,線程相對安全了,避開了過多的同步(因爲這裏的同步只需在第一次創建實例時才同步,一旦創建成功,以後獲取實例時就不需要同獲取鎖了),效率比上面那個能提高一些// volatile關鍵字確保當uniqueInstance變量被初始化成Singleton實例時,多個線程正確地處理uniqueInstance變量,這個關鍵字其實也解決了上面說的系統可能崩潰的問題,因爲使用這個變量也需要一個線程一個線程的來使用了privatevolatilestaticSingleton uniqueInstance;privateSingleton(){    }publicstaticSingletongetInSingleton(){if(uniqueInstance ==null) {// 檢查實例,如是不存在就進行同步代碼區synchronized(Singleton.class){//1 // 對其進行鎖,防止兩個線程同時進入同步代碼區if(uniqueInstance ==null) {//2 // 雙重檢查,非常重要,如果兩個同時訪問的線程,當第一線程訪問完同步代碼區後,生成一個實例;當第二個已進入getInstance方法等待的線程進入同步代碼區時,也會產生一個新的實例uniqueInstance =newSingleton();//3}            }        }returnuniqueInstance;    }// ...Remainder omitted}//使用靜態內部類是沒問題的,而且效率也不會降低,而且還是懶加載classSingleton2{//jvm加載SingletonHolder的時候會初始化INSTANCE,所以既是lazy的又保證是單例的privatestaticclassSingletonHolder{//靜態內部類,只會被加載一次(在加載外部類的時候),所以線程安全,注意靜態只能使用靜態staticfinalSingleton2 INSTANCE =newSingleton2();    }privateSingleton2(){}//靜態構造方法publicstaticSingleton2getInstance(){//對外提供單例的接口returnSingletonHolder.INSTANCE;    }}classceshi{//只是簡單測試了一下單例,都爲true,可以忽略publicstaticvoidmain(String[] args){        System.out.println(danli.STR1 == danli.STR1);//trueSystem.out.println(danli2.getSingleInstance() == danli2.getSingleInstance());        System.out.println(danli3.getSingleInstance() == danli3.getSingleInstance());        System.out.println(Singleton.getInSingleton() == Singleton.getInSingleton());        System.out.println(Singleton2.getInstance() == Singleton2.getInstance());    }}

二、應用部分:單例的適用場景

優點:

第一、能減少資源的使用,但有時需要通過線程同步來控制資源的併發訪問;也避免對共享資源的多重佔用

第二、控制實例產生的數量(允許可變數目的實例),由於在系統內存中只存在一個對象,因此可以 節約系統資源,當 需要頻繁創建和銷燬的對象時單例模式無疑可以提高系統的性能。

第三、作爲通信媒介使用,也就是數據共享,共享這一個對象一個實例(如線程池),它可以在不建立直接關聯的條件下,讓多個不相關的兩個線程或者進程之間實現通信,但注意多線程同步問題。

缺點:

1.不太適用於變化的對象,如果同一類型的對象總是要在不同的用例場景發生變化,單例就會引起數據的錯誤,不能保存彼此的狀態,所以就算保存了,需要加入同步機制來避免錯誤。

2.由於單例模式中沒有抽象層,因此單例類的擴展有很大的困難。

3.單例類的職責過重,在一定程度上違背了“單一職責原則”。

4.濫用單例將帶來一些負面問題,如爲了節省資源將數據庫連接池對象設計爲的單例類,可能會導致共享連接池對象的程序過多而出現連接池溢出;如果實例化的對象長時間不被利用,系統會認爲是垃圾而被回收,這將導致對象狀態的丟失。

使用注意事項:

1.使用時不能用反射模式創建單例,否則會實例化一個新的對象

2.使用懶單例模式時注意線程安全問題

3.餓單例模式和懶單例模式構造方法都是私有的,因而是不能被繼承的,有些單例模式可以被繼承(如登記式模式)

適合場景:

1、有頻繁實例化然後銷燬的情況,也就是頻繁的 new 對象,可以考慮單例模式;

2、創建對象時耗時過多或者耗資源過多,但又經常用到的對象;

3、頻繁訪問 IO 資源的對象,例如數據庫連接池或訪問本地文件;

4、單例模式只允許創建一個對象,因此節省內存,加快對象訪問速度,因此對象需要被公用的場合適合使用,如多個模塊使用同一個數據源連接對象等等。

具體應用場景舉例:

外部資源:每臺計算機有若干個打印機,但只能有一個PrinterSpooler,以避免兩個打印作業同時輸出到打印機。

內部資源:大多數軟件都有一個(或多個)屬性文件存放系統配置,這樣的系統應該有一個對象管理這些屬性文件,在我們日常使用的在Windows中也有不少單例模式設計的組件,象常用的文件管理器。由於Windows操作系統是一個典型的多進程多線程系統,那麼在創建或者刪除某個文件的時候,就不可避免地出現多個進程或線程同時操作一個文件的現象。採用單例模式設計的文件管理器就可以完美的解決這個問題,所有的文件操作都必須通過唯一的實例進行,這樣就不會產生混亂的現象。

Windows的Task Manager(任務管理器)就是很典型的單例模式(這個很熟悉吧),想想看,是不是呢,你能打開兩個windows task manager嗎? 不信你自己試試看哦~

windows的Recycle Bin(回收站)也是典型的單例應用。在整個系統運行過程中,回收站一直維護着僅有的一個實例。

網站的計數器,一般也是採用單例模式實現,否則難以同步。

應用程序的日誌應用,一般都何用單例模式實現,這一般是由於共享的日誌文件一直處於打開狀態,因爲只能有一個實例去操作,否則內容不好追加。

Web應用的配置對象的讀取,一般也應用單例模式,這個是由於配置文件是共享的資源。

數據庫連接池的設計一般採用單例模式,數據庫連接是一種數據庫資源。軟件系統中使用數據庫連接池,主要是節省打開或者關閉數據庫連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的。當然,使用數據庫連接池還有很多其它的好處,可以屏蔽不同數據數據庫之間的差異,實現系統對數據庫的低度耦合,也可以被多個系統同時使用,具有高可複用性,還能方便對數據庫連接的管理等等。數據庫連接池屬於重量級資源,一個應用中只需要保留一份即可,既節省了資源又方便管理。所以數據庫連接池採用單例模式進行設計會是一個非常好的選擇。

多線程的線程池的設計一般也是採用單例模式,這是由於線程池要方便對池中的線程進行控制。

spring的bean(scope)默認是single,當然也可以當然也可以設置爲prototype,比如struts2的action就必須是prototype,因爲請求不同,一個請求對應一個action對象。

我們知道單例會發生線程安全問題,那麼spring是怎麼來解決的呢?

問題:當Bean對象對應的類存在可變的成員變量並且其中存在改變這個變量的線程時,多線程操作該Bean對象時會出現線程安全。原因:當多線程中存在線程改變了bean對象的可變成員變量時,其他線程無法訪問該bean對象的初始狀態,從而造成數據錯亂解決方式:1.在Bean對象中儘量避免定義可變的成員變量;2.在bean對象中定義一個ThreadLocal成員變量,將需要的可變成員變量保存在ThreadLocal中

2個具體場景案例

1、網站在線人數統計;

其實就是全局計數器,也就是說所有用戶在相同的時刻獲取到的在線人數數量都是一致的。要實現這個需求,計數器就要全局唯一,也就正好可以用單例模式來實現。當然這裏不包括分佈式場景,因爲計數是存在內存中的,並且還要保證線程安全。下面代碼是一個簡單的計數器實現。

publicclassCounter{privatestaticclassCounterHolder{privatestaticfinalCounter counter =newCounter();    }privateCounter(){        System.out.println("init...");    }publicstaticfinalCountergetInstance(){returnCounterHolder.counter;    }privateAtomicLong online =newAtomicLong();publiclonggetOnline(){returnonline.get();    }publiclongadd(){returnonline.incrementAndGet();    }    .......}

1、配置文件訪問類;

項目中經常需要一些環境相關的配置文件,比如短信通知相關的、郵件相關的。比如 properties 文件,這裏就以讀取一個properties 文件配置爲例,如果你使用的 Spring ,可以用 @PropertySource 註解實現,默認就是單例模式。如果不用單例的話,每次都要 new 對象,每次都要重新讀一遍配置文件,很影響性能,如果用單例模式,則只需要讀取一遍就好了。以下是文件訪問單例類簡單實現:

publicclassSingleProperty{privatestaticProperties prop;privatestaticclassSinglePropertyHolder{privatestaticfinalSingleProperty singleProperty =newSingleProperty();    }/**

    * config.properties 內容是 test.name=kite

    */privateSingleProperty(){        System.out.println("構造函數執行");        prop =newProperties();        InputStream stream = SingleProperty.class.getClassLoader()                .getResourceAsStream("config.properties");try{            prop.load(newInputStreamReader(stream,"utf-8"));        }catch(IOException e) {            e.printStackTrace();        }    }publicstaticSinglePropertygetInstance(){returnSinglePropertyHolder.singleProperty;    }publicStringgetName(){returnprop.get("test.name").toString();    }publicstaticvoidmain(String[] args){        SingleProperty singleProperty = SingleProperty.getInstance();        System.out.println(singleProperty.getName());    }}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章