|
不管是設(shè)計模式也好,別的模式也要,他都是為了解決問題而發(fā)明的有效的方法。除了我們已經(jīng)熟悉的23種設(shè)計模式以外,還有MVVM、Combinator等其它的東西,都已經(jīng)是前輩們經(jīng)過多年的摸爬滾打總結(jié)出來的,其有效性不容置疑。我這篇文章也不會用來證明設(shè)計模式是有用的,因為在我看來,這就跟1+1=2一樣明顯(在黑板上寫下1+1=2)
而這,在現(xiàn)在這個追求高質(zhì)量代碼的時代,雖然顯得有一些復(fù)雜,但是我個人還是“推崇”(看好了,我有引號的)這個東西,畢竟面試必問系列,你咋整
來看今天的內(nèi)容吧,有代碼,有實例,并且有一些內(nèi)容我直接放在代碼中通過注釋進行講解,會更好理解
文章首發(fā)公眾號:Java架構(gòu)師聯(lián)盟,每日更新技術(shù)好文
一、設(shè)計部分:單例的實現(xiàn)思想、代碼及注意問題 package com.test.hibernate;
/*生成一個懶漢式單例的基礎(chǔ)理解: 1.Singleton顧名思義就是只能創(chuàng)建一個實例對象。。所以不能擁有public的構(gòu)造方法 2.既然構(gòu)造方法是私有的,那么從外面不可能創(chuàng)建Singleton實例了。。只能從內(nèi)部創(chuàng)建。。所以需要一個方法來創(chuàng)建此實例,也因此只能通過類名來創(chuàng)建對象。。此方法肯定必須是static的 3.靜態(tài)的getInstance方法要返回一個Singleton實例。。就要一個Singleton類型的變量來存儲。。聲明一個Singleton類型的屬性。。同樣需要是static 的。。靜態(tài)方法只能訪問靜態(tài)屬性。。。 ?。。∏?步是單例的共性!后三步是懶漢式需要考慮的地方! 4.為了保證只生成一個實例,需要做判斷是否為null 5.此時考慮線程問題,假設(shè)有兩個線程。。thread1,thread2。。thread1運行到判斷那個Singleton類型的變量是否為null,然后跳到了thread2。。也運行到判斷之后。。。此時兩線程都得到single為空。。。那么就會有兩個實例了。。。解決辦法。。同步 6.同步又要考慮效率,不能有太多的沒用同步 * */
//思考總結(jié):其實餓漢式?jīng)]什么問題,問題就是出現(xiàn)在懶漢式上,一般就是牽扯到執(zhí)行效率和線程安全2個角度上來思考 //個人感覺 如果是餓漢式就用天然的沒毛病,如果想用懶漢式就用靜態(tài)內(nèi)部類方式吧 //存在問題:如何在2個jvm上保證單例還未解決:這個就牽扯到分布式鎖,可以用zookeeper來實現(xiàn) //還缺少一種懶漢式的枚舉方式實現(xiàn)有待研究,聽說這個方法也不錯。
public class danli { //模擬一下靜態(tài)代碼塊的使用方式,靜態(tài)代碼塊在類加載的運行,先靜態(tài)代碼塊》再構(gòu)造代碼塊》再構(gòu)造函數(shù) ,只研究單例可以忽略
public static final String STR1; static { STR1 = new String("zzh"); }
} //下面正式演示各種單例的實現(xiàn):
class danli2{//單例餓漢式(非延時加載),提前加載,有利于速度和反應(yīng)時間,天然的線程安全的。沒毛病 private danli2(){}; private static final danli2 two = new danli2();//final可加可不加,final的目的就是最終的,只允許一次賦值,但不加是因為沒法在本類外給他賦值了,因為構(gòu)造方法是私有的沒法創(chuàng)建這個類的對象了,而且這個成員變量也是私有的所以不能在外面調(diào)用到,但是可以在本類中的其他方法調(diào)用到,所以其實還是可以修改的,所以還是加上final吧 public static danli2 getSingleInstance(){ return two; } }
class danli3{ //單例懶漢式(延時加載),用的時候再去加載,有利于資源充分利用 private danli3(){}; private static danli3 three = null; public static synchronized danli3 getSingleInstance(){//加上synchronized變得線程安全了,但是效率下降了,每次還需要檢查同步等等 if(three == null){//保證只生成一個實例 three = new danli3(); } return three; } } /* 該類跟上面那個是一樣的,上面是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的判斷,解決除第一次以外所有的同步判斷導(dǎo)致的效率下降問題 //但是這個雙重檢查加鎖在多線程環(huán)境下存在系統(tǒng)崩潰的可能(一個線程初始化一半的對象,被第二個線程直接拿去用了,所以系統(tǒng)崩潰了) /*原因如下 1、線程 1 進入 getInSingleton() 方法。 2、由于 uniqueInstance 為 null,線程 1 在 //1 處進入 synchronized 塊。 3、線程 1 前進到 //3 處,但在構(gòu)造函數(shù)執(zhí)行之前,使實例成為非 null。 4、線程 1 被線程 2 預(yù)占。 5、線程 2 檢查實例是否為 null。因為實例不為 null,線程 2 將 uniqueInstance 引用返回給一個構(gòu)造完整但部分初始化了的 Singleton 對象。 6、線程 2 被線程 1 預(yù)占。 7、線程 1 通過運行 Singleton 對象的構(gòu)造函數(shù)并將引用返回給它,來完成對該對象的初始化。
*/ class Singleton {//雙重檢查加鎖,線程相對安全了,避開了過多的同步(因為這里的同步只需在第一次創(chuàng)建實例時才同步,一旦創(chuàng)建成功,以后獲取實例時就不需要同獲取鎖了),效率比上面那個能提高一些 // volatile關(guān)鍵字確保當uniqueInstance變量被初始化成Singleton實例時,多個線程正確地處理uniqueInstance變量,這個關(guān)鍵字其實也解決了上面說的系統(tǒng)可能崩潰的問題,因為使用這個變量也需要一個線程一個線程的來使用了 private volatile static Singleton uniqueInstance; private Singleton() { }
public static Singleton getInSingleton() { if (uniqueInstance == null) {// 檢查實例,如是不存在就進行同步代碼區(qū) synchronized (Singleton.class) {//1 // 對其進行鎖,防止兩個線程同時進入同步代碼區(qū) if (uniqueInstance == null) {//2 // 雙重檢查,非常重要,如果兩個同時訪問的線程,當?shù)谝痪€程訪問完同步代碼區(qū)后,生成一個實例;當?shù)诙€已進入getInstance方法等待的線程進入同步代碼區(qū)時,也會產(chǎn)生一個新的實例 uniqueInstance = new Singleton();//3 } } } return uniqueInstance; } // ...Remainder omitted }
//使用靜態(tài)內(nèi)部類是沒問題的,而且效率也不會降低,而且還是懶加載 class Singleton2 {//jvm加載SingletonHolder的時候會初始化INSTANCE,所以既是lazy的又保證是單例的 private static class SingletonHolder {//靜態(tài)內(nèi)部類,只會被加載一次(在加載外部類的時候),所以線程安全,注意靜態(tài)只能使用靜態(tài) static final Singleton2 INSTANCE = new Singleton2(); }
private Singleton2 (){}//靜態(tài)構(gòu)造方法
public static Singleton2 getInstance() {//對外提供單例的接口 return SingletonHolder.INSTANCE; } }
class ceshi{//只是簡單測試了一下單例,都為true,可以忽略 public static void main(String[] args) { System.out.println(danli.STR1 == danli.STR1);//true System.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()); } } 二、應(yīng)用部分:單例的適用場景 優(yōu)點: 第一、能減少資源的使用,但有時需要通過線程同步來控制資源的并發(fā)訪問;也避免對共享資源的多重占用
第二、控制實例產(chǎn)生的數(shù)量(允許可變數(shù)目的實例),由于在系統(tǒng)內(nèi)存中只存在一個對象,因此可以 節(jié)約系統(tǒng)資源,當 需要頻繁創(chuàng)建和銷毀的對象時單例模式無疑可以提高系統(tǒng)的性能。
第三、作為通信媒介使用,也就是數(shù)據(jù)共享,共享這一個對象一個實例(如線程池),它可以在不建立直接關(guān)聯(lián)的條件下,讓多個不相關(guān)的兩個線程或者進程之間實現(xiàn)通信,但注意多線程同步問題。
缺點: 1.不太適用于變化的對象,如果同一類型的對象總是要在不同的用例場景發(fā)生變化,單例就會引起數(shù)據(jù)的錯誤,不能保存彼此的狀態(tài),所以就算保存了,需要加入同步機制來避免錯誤。 2.由于單例模式中沒有抽象層,因此單例類的擴展有很大的困難。 3.單例類的職責過重,在一定程度上違背了“單一職責原則”。 4.濫用單例將帶來一些負面問題,如為了節(jié)省資源將數(shù)據(jù)庫連接池對象設(shè)計為的單例類,可能會導(dǎo)致共享連接池對象的程序過多而出現(xiàn)連接池溢出;如果實例化的對象長時間不被利用,系統(tǒng)會認為是垃圾而被回收,這將導(dǎo)致對象狀態(tài)的丟失。
使用注意事項: 1.使用時不能用反射模式創(chuàng)建單例,否則會實例化一個新的對象 2.使用懶單例模式時注意線程安全問題 3.餓單例模式和懶單例模式構(gòu)造方法都是私有的,因而是不能被繼承的,有些單例模式可以被繼承(如登記式模式)
適合場景: 1、有頻繁實例化然后銷毀的情況,也就是頻繁的 new 對象,可以考慮單例模式;
2、創(chuàng)建對象時耗時過多或者耗資源過多,但又經(jīng)常用到的對象;
3、頻繁訪問 IO 資源的對象,例如數(shù)據(jù)庫連接池或訪問本地文件;
4、單例模式只允許創(chuàng)建一個對象,因此節(jié)省內(nèi)存,加快對象訪問速度,因此對象需要被公用的場合適合使用,如多個模塊使用同一個數(shù)據(jù)源連接對象等等。
具體應(yīng)用場景舉例: 外部資源:每臺計算機有若干個打印機,但只能有一個PrinterSpooler,以避免兩個打印作業(yè)同時輸出到打印機。 內(nèi)部資源:大多數(shù)軟件都有一個(或多個)屬性文件存放系統(tǒng)配置,這樣的系統(tǒng)應(yīng)該有一個對象管理這些屬性文件,在我們?nèi)粘J褂玫脑赪indows中也有不少單例模式設(shè)計的組件,象常用的文件管理器。由于Windows操作系統(tǒng)是一個典型的多進程多線程系統(tǒng),那么在創(chuàng)建或者刪除某個文件的時候,就不可避免地出現(xiàn)多個進程或線程同時操作一個文件的現(xiàn)象。采用單例模式設(shè)計的文件管理器就可以完美的解決這個問題,所有的文件操作都必須通過唯一的實例進行,這樣就不會產(chǎn)生混亂的現(xiàn)象。 Windows的Task Manager(任務(wù)管理器)就是很典型的單例模式(這個很熟悉吧),想想看,是不是呢,你能打開兩個windows task manager嗎? 不信你自己試試看哦~ windows的Recycle Bin(回收站)也是典型的單例應(yīng)用。在整個系統(tǒng)運行過程中,回收站一直維護著僅有的一個實例。 網(wǎng)站的計數(shù)器,一般也是采用單例模式實現(xiàn),否則難以同步。 應(yīng)用程序的日志應(yīng)用,一般都何用單例模式實現(xiàn),這一般是由于共享的日志文件一直處于打開狀態(tài),因為只能有一個實例去操作,否則內(nèi)容不好追加。 Web應(yīng)用的配置對象的讀取,一般也應(yīng)用單例模式,這個是由于配置文件是共享的資源。 數(shù)據(jù)庫連接池的設(shè)計一般采用單例模式,數(shù)據(jù)庫連接是一種數(shù)據(jù)庫資源。軟件系統(tǒng)中使用數(shù)據(jù)庫連接池,主要是節(jié)省打開或者關(guān)閉數(shù)據(jù)庫連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的。當然,使用數(shù)據(jù)庫連接池還有很多其它的好處,可以屏蔽不同數(shù)據(jù)數(shù)據(jù)庫之間的差異,實現(xiàn)系統(tǒng)對數(shù)據(jù)庫的低度耦合,也可以被多個系統(tǒng)同時使用,具有高可復(fù)用性,還能方便對數(shù)據(jù)庫連接的管理等等。數(shù)據(jù)庫連接池屬于重量級資源,一個應(yīng)用中只需要保留一份即可,既節(jié)省了資源又方便管理。所以數(shù)據(jù)庫連接池采用單例模式進行設(shè)計會是一個非常好的選擇。 多線程的線程池的設(shè)計一般也是采用單例模式,這是由于線程池要方便對池中的線程進行控制。 spring的bean(scope)默認是single,當然也可以當然也可以設(shè)置為prototype,比如struts2的action就必須是prototype,因為請求不同,一個請求對應(yīng)一個action對象。 我們知道單例會發(fā)生線程安全問題,那么spring是怎么來解決的呢? 問題:當Bean對象對應(yīng)的類存在可變的成員變量并且其中存在改變這個變量的線程時,多線程操作該Bean對象時會出現(xiàn)線程安全。原因:當多線程中存在線程改變了bean對象的可變成員變量時,其他線程無法訪問該bean對象的初始狀態(tài),從而造成數(shù)據(jù)錯亂解決方式:1.在Bean對象中盡量避免定義可變的成員變量;2.在bean對象中定義一個ThreadLocal成員變量,將需要的可變成員變量保存在ThreadLocal中 2個具體場景案例 1、網(wǎng)站在線人數(shù)統(tǒng)計;
其實就是全局計數(shù)器,也就是說所有用戶在相同的時刻獲取到的在線人數(shù)數(shù)量都是一致的。要實現(xiàn)這個需求,計數(shù)器就要全局唯一,也就正好可以用單例模式來實現(xiàn)。當然這里不包括分布式場景,因為計數(shù)是存在內(nèi)存中的,并且還要保證線程安全。下面代碼是一個簡單的計數(shù)器實現(xiàn)。
public class Counter { private static class CounterHolder{ private static final Counter counter = new Counter(); } private Counter(){ System.out.println("init..."); } public static final Counter getInstance(){ return CounterHolder.counter; } private AtomicLong online = new AtomicLong(); public long getOnline(){ return online.get(); } public long add(){ return online.incrementAndGet(); } ....... } 1、配置文件訪問類;
項目中經(jīng)常需要一些環(huán)境相關(guān)的配置文件,比如短信通知相關(guān)的、郵件相關(guān)的。比如 properties 文件,這里就以讀取一個properties 文件配置為例,如果你使用的 Spring ,可以用 @PropertySource 注解實現(xiàn),默認就是單例模式。如果不用單例的話,每次都要 new 對象,每次都要重新讀一遍配置文件,很影響性能,如果用單例模式,則只需要讀取一遍就好了。以下是文件訪問單例類簡單實現(xiàn):
public class SingleProperty { private static Properties prop; private static class SinglePropertyHolder{ private static final SingleProperty singleProperty = new SingleProperty(); } /** * config.properties 內(nèi)容是 test.name=kite */ private SingleProperty(){ System.out.println("構(gòu)造函數(shù)執(zhí)行"); prop = new Properties(); InputStream stream = SingleProperty.class.getClassLoader() .getResourceAsStream("config.properties"); try { prop.load(new InputStreamReader(stream, "utf-8")); } catch (IOException e) { e.printStackTrace(); } } public static SingleProperty getInstance(){ return SinglePropertyHolder.singleProperty; } public String getName(){ return prop.get("test.name").toString(); } public static void main(String[] args){ SingleProperty singleProperty = SingleProperty.getInstance(); System.out.println(singleProperty.getName()); } } 深入靈魂的考驗,每行注釋都是靈魂的單例模式,源碼+實例降臨
|