單例模式的優(yōu)與劣(單例模式優(yōu)缺點)

      網(wǎng)友投稿 961 2025-04-05

      一、前言

      首先來明確一個問題,那就是在某些情況下,有些對象,我們只需要一個就可以了,比如,一臺計算機上可以連好幾個打印機,但是這個計算機上的打印程序只能有一個,這里就可以通過單例模式來避免兩個打印作業(yè)同時輸出到打印機中,即在整個的打印過程中我只有一個打印程序的實例。

      簡單說來,單例模式(也叫單件模式)的作用就是保證在整個應用程序的生命周期中,任何一個時刻,單例類的實例都只存在一個(當然也可以不存在)。

      下圖是單例模式的結構圖。

      下面就來看一種情況(這里先假設我的應用程序是多線程應用程序),示例代碼如下:

      public static Singleton GetInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; }

      如果在一開始調(diào)用 GetInstance()時,是由兩個線程同時調(diào)用的(這種情況是很常見的),注意是同時,(或者是一個線程進入 if 判斷語句后但還沒有實例化 Singleton 時,第二個線程到達,此時 singleton 還是為 null)這樣的話,兩個線程均會進入 GetInstance(),而后由于是第一次調(diào)用 GetInstance(),所以存儲在 Singleton 中的靜態(tài)變量 singleton 為 null ,這樣的話,就會讓兩個線程均通過 if 語句的條件判斷,然后調(diào)用 new Singleton()了,這樣的話,問題就出來了,因為有兩個線程,所以會創(chuàng)建兩個實例,很顯然,這便違法了單例模式的初衷了,那么如何解決上面出現(xiàn)的這個問題(即多線程下使用單例模式時有可能會創(chuàng)建多個實例這一現(xiàn)象)呢?

      其實,這個是很好解決的,可以這樣思考這個問題:由于上面出現(xiàn)的問題中涉及到多個線程同時訪問這個 GetInstance(),那么可以先將一個線程鎖定,然后等這個線程完成以后,再讓其他的線程訪問 GetInstance()中的 if 段語句。示例代碼如下:

      public static Singleton GetInstance() { lock(syncRoot){ if (singleton == null) { singleton = new Singleton(); } } return singleton; }

      但是如果這樣的話,每次調(diào)用GetInstance方法時都需要lock操作,影響性能。

      下面就來重新改進前面 Demo 中的 Singleton 類,使其在多線程的環(huán)境下也可以實現(xiàn)單例模式的功能。

      namespace Singleton { public class Singleton { //定義一個私有的靜態(tài)全局變量來保存該類的唯一實例 private static Singleton singleton; //定義一個只讀靜態(tài)對象,且這個對象是在程序運行時創(chuàng)建的。 private static readonly object syncObject = new object(); //構造函數(shù)必須是私有的,這樣在外部便無法使用 new 來創(chuàng)建該類的實例 private Singleton(){} //定義一個全局訪問點,設置為靜態(tài)方法,則在類的外部便無需實例化就可以調(diào)用該方法 public static Singleton GetInstance() { //這里可以保證只實例化一次,即在第一次調(diào)用時實例化,以后調(diào)用便不會再實例化 //第一重 singleton == null if (singleton == null) { lock (syncObject) { //第二重 singleton == null if (singleton == null) { singleton = new Singleton(); } } } return singleton; } } }

      上面的就是改進后的代碼,可以看到在類中有定義了一個靜態(tài)的只讀對象syncObject,這里需要說明的是,為何還要創(chuàng)建一個 syncObject 靜態(tài)只讀對象呢?

      由于提供給 lock 關鍵字的參數(shù)必須為基于引用類型的對象,該對象用來定義鎖的范圍,所以這個引用類型的對象總不能為 null 吧,而一開始的時候,singleton 為 null ,所以是無法實現(xiàn)加鎖的,所以必須要再創(chuàng)建一個對象即 syncObject 來定義加鎖的范圍。

      還有要解釋一下的就是在 GetInstance()中,我為什么要在 if 語句中使用兩次判斷 singleton == null ,這里涉及到一個名詞 Double-Check Locking ,也就是雙重檢查鎖定,為何要使用雙重檢查鎖定呢?

      考慮這樣一種情況,就是有兩個線程同時到達,即同時調(diào)用 GetInstance(),此時由于 singleton == null ,所以很明顯,兩個線程都可以通過第一重的 singleton == null ,進入第一重 if 語句后,由于存在鎖機制,所以會有一個線程進入 lock 語句并進入第二重 singleton == null ,而另外的一個線程則會在 lock 語句的外面等待。

      而當?shù)谝粋€線程執(zhí)行完 new Singleton()語句后,便會退出鎖定區(qū)域,此時,第二個線程便可以進入 lock 語句塊,此時,如果沒有第二重 singleton == null 的話,那么第二個線程還是可以調(diào)用 new Singleton()語句,這樣第二個線程也會創(chuàng)建一個 Singleton 實例,這樣也還是違背了單例模式的初衷的,所以這里必須要使用雙重檢查鎖定。

      細心的朋友一定會發(fā)現(xiàn),如果我去掉第一重 singleton == null ,程序還是可以在多線程下完好的運行的,考慮在沒有第一重 singleton == null 的情況下,當有兩個線程同時到達,此時,由于 lock 機制的存在,第一個線程會進入 lock 語句塊,并且可以順利執(zhí)行 new Singleton(),當?shù)谝粋€線程退出 lock 語句塊時, singleton 這個靜態(tài)變量已不為 null 了,所以當?shù)诙€線程進入 lock 時,還是會被第二重 singleton == null 擋在外面,而無法執(zhí)行 new Singleton(),所以在沒有第一重 singleton == null 的情況下,也是可以實現(xiàn)單例模式的?那么為什么需要第一重 singleton == null 呢?

      這里就涉及一個性能問題了,因為對于單例模式的話,new Singleton()只需要執(zhí)行一次就 OK 了,而如果沒有第一重 singleton == null 的話,每一次有線程進入 GetInstance()時,均會執(zhí)行鎖定操作來實現(xiàn)線程同步,這是非常耗費性能的,而如果我加上第一重 singleton == null 的話,那么就只有在第一次,也就是 singleton ==null 成立時的情況下執(zhí)行一次鎖定以實現(xiàn)線程同步,而以后的話,便只要直接返回 Singleton 實例就 OK 了而根本無需再進入 lock 語句塊了,這樣就可以解決由線程同步帶來的性能問題了。

      好,關于多線程下單例模式的實現(xiàn)的介紹就到這里了,但是,關于單例模式的介紹還沒完。

      二、單例的三種實現(xiàn)方式

      下面將要介紹的是懶漢式單例和餓漢式單例

      2.1 懶漢式單例

      何為懶漢式單例呢,可以這樣理解,單例模式呢,其在整個應用程序的生命周期中只存在一個實例,懶漢式呢,就是這個單例類的這個唯一實例是在第一次使用 GetInstance()時實例化的,如果不調(diào)用 GetInstance()的話,這個實例是不會存在的,即為 null。

      形象點說呢,就是你不去動它的話,它自己是不會實例化的,所以可以稱之為懶漢。

      其實呢,我前面在介紹單例模式的這幾個 Demo 中都是使用的懶漢式單例,看下面的 GetInstance()方法就明白了:

      // 延遲初始化保證線程安全(禁止重排序) private volatile static Singleton singleton = null; private Singleton(){} public static Singleton GetInstance() { if (singleton == null) { lock (syncObject) // synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }

      從上面的這個 GetInstance()中可以看出這個單例類的唯一實例是在第一次調(diào)用 GetInstance()時實例化的,所以此為懶漢式單例。

      另外,可以看到里面加了volatile關鍵字來聲明單例對象,既然synchronized已經(jīng)起到了多線程下原子性、有序性、可見性的作用,為什么還要加volatile呢?見參考文獻。

      雙重檢測鎖定失敗的問題并不歸咎于 JVM 中的實現(xiàn) bug,而是歸咎于 Java 平臺內(nèi)存模型。內(nèi)存模型允許所謂的“無序寫入”,這也是失敗的一個主要原因。因此,為了杜絕“無序寫入”的出現(xiàn),使用voaltile關鍵字。

      2.2 餓漢式單例

      上面介紹了懶漢式單例,到這里來理解餓漢式單例的話,就容易多了。懶漢式單例是不會主動實例化單例類的唯一實例的,而餓漢式的話,則剛好相反,他會以靜態(tài)初始化的方式在自己被加載時就將自己實例化。

      下面就來看一看餓漢式單例類

      //餓漢式單例類.在類初始化時,已經(jīng)自行實例化 public class Singleton1 { //私有的默認構造器 private Singleton1() {} //已經(jīng)自行實例化 private static final Singleton single = new Singleton(); //靜態(tài)工廠方法 public static Singleton getInstance() { return single; } }

      上面的餓漢式單例類中可以看到,當整個類被加載的時候,就會自行初始化 singleton 這個靜態(tài)只讀變量。而非在第一次調(diào)用 GetInstance()時再來實例化單例類的唯一實例,所以這就是一種餓漢式的單例類。

      2.3 登記式單例類(可忽略)

      import java.util.HashMap; import java.util.Map; //登記式單例類. //類似Spring里面的方法,將類名注冊,下次從里面直接獲取。 public class Singleton3 { private static Map map = new HashMap(); static{ Singleton3 single = new Singleton3(); map.put(single.getClass().getName(), single); } //保護的默認構造器 protected Singleton3(){} //靜態(tài)工廠方法,返還此類惟一的實例 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); } //一個示意性的商業(yè)方法 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(登記薄)中,對于已經(jīng)登記過的實例,則從Map直接返回,對于沒有登記的,則先登記,然后返回。

      這里我對登記式單例標記了可忽略,我的理解來說,首先它用的比較少,另外其實內(nèi)部實現(xiàn)還是用的餓漢式單例,因為其中的static方法塊,它的單例在類被裝載的時候就被實例化了。

      好,到這里,就真正的把單例模式介紹完了,在此呢再總結一下單例類需要注意的幾點:

      一、單例模式是用來實現(xiàn)在整個程序中只有一個實例的。

      二、單例類的構造函數(shù)必須為私有,同時單例類必須提供一個全局訪問點。

      三、單例模式在多線程下的同步問題和性能問題的解決。

      四、懶漢式和餓漢式單例類。

      三、餓漢式與懶漢式的區(qū)別

      從速度和反應時間角度來講,非延遲加載(又稱餓漢式)好;從資源利用效率上說,延遲加載(又稱懶漢式)好。

      餓漢式天生就是線程安全的,可以直接用于多線程而不會出現(xiàn)問題;懶漢式本身是非線程安全的,為了實現(xiàn)線程安全需附加語句。

      餓漢式在類創(chuàng)建的同時就實例化一個靜態(tài)對象出來,不管之后會不會使用這個單例,都會占據(jù)一定的內(nèi)存,但是相應的,在第一次調(diào)用時速度也會更快,因為其資源已經(jīng)初始化完成。而懶漢式顧名思義,會延遲加載,在第一次使用該單例的時候才會實例化對象出來,如果要做的工作比較多,性能上會有些延遲,之后就和餓漢式一樣了。

      四、單例對象作配置信息管理時可能會帶來的幾個同步問題

      1.在多線程環(huán)境下,單例對象的同步問題主要體現(xiàn)在兩個方面,單例對象的初始化和單例對象的屬性更新。

      本文描述的方法有如下假設:

      單例對象的屬性(或成員變量)的獲取是通過單例對象的初始化實現(xiàn)的。也就是說,在單例對象初始化時,會從文件或數(shù)據(jù)庫中讀取最新的配置信息。

      其他對象不能直接改變單例對象的屬性,單例對象屬性的變化來源于配置文件或配置數(shù)據(jù)庫數(shù)據(jù)的變化。

      4.1單例對象的初始化

      首先,討論一下單例對象的初始化同步。單例模式的通常處理方式是,在對象中有一個靜態(tài)成員變量,其類型就是單例類型本身;如果該變量為null,則創(chuàng)建該單例類型的對象,并將該變量指向這個對象;如果該變量不為null,則直接使用該變量。

      這種處理方式在單線程的模式下可以很好的運行;但是在多線程模式下,可能產(chǎn)生問題。如果第一個線程發(fā)現(xiàn)成員變量為null,準備創(chuàng)建對象;這是第二個線程同時也發(fā)現(xiàn)成員變量為null,也會創(chuàng)建新對象。這就會造成在一個JVM中有多個單例類型的實例。如果這個單例類型的成員變量在運行過程中變化,會造成多個單例類型實例的不一致,產(chǎn)生一些很奇怪的現(xiàn)象。例如,某服務進程通過檢查單例對象的某個屬性來停止多個線程服務,如果存在多個單例對象的實例,就會造成部分線程服務停止,部分線程服務不能停止的情況(此時可考慮使用雙重鎖安全機制)。

      4.2 單例對象的屬性更新

      通常,為了實現(xiàn)配置信息的實時更新,會有一個線程不停檢測配置文件或配置數(shù)據(jù)庫的內(nèi)容,一旦發(fā)現(xiàn)變化,就更新到單例對象的屬性中。在更新這些信息的時候,很可能還會有其他線程正在讀取這些信息,造成意想不到的后果。還是以通過單例對象屬性停止線程服務為例,如果更新屬性時讀寫不同步,可能訪問該屬性時這個屬性正好為空(null),程序就會拋出異常。

      下面是解決方法

      //單例對象的初始化同步 public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance == null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance == null) { syncInit(); } return instance; } public Vector getProperties() { return properties; } }

      這種處理方式雖然引入了同步代碼,但是因為這段同步代碼只會在最開始的時候執(zhí)行一次或多次,所以對整個系統(tǒng)的性能不會有影響。

      參照讀者/寫者的處理方式,設置一個讀計數(shù)器,每次讀取配置信息前,將計數(shù)器加1,讀完后將計數(shù)器減1.只有在讀計數(shù)器為0時,才能更新數(shù)據(jù),同時要阻塞所有讀屬性的調(diào)用。

      代碼如下:

      public class GlobalConfig { private static GlobalConfig instance; private Vector properties = null; private boolean isUpdating = false; private int readCount = 0; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance == null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance==null) { syncInit(); } return instance; } public synchronized void update(String p_data) { syncUpdateIn(); //Update properties } private synchronized void syncUpdateIn() { while (readCount > 0) { try { wait(); } catch (Exception e) { } } } private synchronized void syncReadIn() { readCount++; } private synchronized void syncReadOut() { readCount--; notifyAll(); } public Vector getProperties() { syncReadIn(); //Process data syncReadOut(); return properties; } }

      采用"影子實例"的辦法。具體說,就是在更新屬性時,直接生成另一個單例對象實例,這個新生成的單例對象實例將從數(shù)據(jù)庫或文件中讀取最新的配置信息;然后將這些配置信息直接賦值給舊單例對象的屬性。

      public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance = null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance = null) { syncInit(); } return instance; } public Vector getProperties() { return properties; } public void updateProperties() { //Load updated configuration information by new a GlobalConfig object GlobalConfig shadow = new GlobalConfig(); properties = shadow.getProperties(); } }

      注意:在更新方法中,通過生成新的GlobalConfig的實例,從文件或數(shù)據(jù)庫中得到最新配置信息,并存放到properties屬性中。上面兩個方法比較起來,第二個方法更好,首先,編程更簡單;其次,沒有那么多的同步操作,對性能的影響也不大。

      五、全局變量和單例模式的區(qū)別

      首先,全局變量就是對一個對象的靜態(tài)引用,全局變量確實可以提供單例模式實現(xiàn)的全局訪問這個功能。但是,它并不能保證應用程序中只有一個實例。

      同時,在編碼規(guī)范中,也明確指出,應該要少用全局變量,因為過多的使用全局變量,會造成代碼難讀。

      還有就是全局變量并不能實現(xiàn)繼承(雖然單例模式在繼承上也不能很好的處理,但是還是可以實現(xiàn)繼承的)而單例模式的話,其在類中保存了它的唯一實例,這個類,它可以保證只能創(chuàng)建一個實例,同時,它還提供了一個訪問該唯一實例的全局訪問點。

      六、單例模式的優(yōu)與劣

      言歸正傳,回到“單例模式的利與弊”問題上來。總結如下:

      6.1 主要優(yōu)點

      提供了對唯一實例的受控訪問。

      由于在系統(tǒng)內(nèi)存中只存在一個對象,因此可以節(jié)約系統(tǒng)資源,對于一些需要頻繁創(chuàng)建和銷毀的對象,單例模式無疑可以提高系統(tǒng)的性能。

      允許可變數(shù)目的實例。

      6.2 主要缺點

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

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

      濫用單例將帶來一些負面問題,如為了節(jié)省資源將數(shù)據(jù)庫連接池對象設計為單例類,可能會導致共享連接池對象的程序過多而出現(xiàn)連接池溢出;如果實例化的對象長時間不被利用,系統(tǒng)會認為是垃圾而被回收,這將導致對象狀態(tài)的丟失。

      公司面試中,“觀察者模式”(發(fā)布者-訂閱者模式)也會被經(jīng)常問到及寫出代碼,詳見博文《大話設計模式(五)觀察者模式》。

      單例模式的優(yōu)與劣(單例模式優(yōu)缺點)

      Java 任務調(diào)度

      版權聲明:本文內(nèi)容由網(wǎng)絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權內(nèi)容。

      版權聲明:本文內(nèi)容由網(wǎng)絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權內(nèi)容。

      上一篇:如何使用頁面布局“主題”?(頁面布局的主題怎么用)
      下一篇:演講實錄,發(fā)現(xiàn)視屏和音頻不同步(錄的視頻的聲音和畫面不同步怎么解決)
      相關文章
      亚洲一卡2卡3卡4卡5卡6卡| 99ri精品国产亚洲| 亚洲免费在线观看视频| 久久亚洲春色中文字幕久久久| 国产偷窥女洗浴在线观看亚洲 | 亚洲精品无码久久| 亚洲精品理论电影在线观看| 国产亚洲sss在线播放| 亚洲ts人妖网站| 亚洲资源最新版在线观看| 亚洲av一本岛在线播放| 亚洲人妖女同在线播放| 亚洲婷婷第一狠人综合精品| 亚洲国色天香视频| 亚洲一级视频在线观看| 国产色在线|亚洲| 亚洲欧美成人一区二区三区| 亚洲男同gay片| 国产成人亚洲精品播放器下载| 国产精品久久亚洲一区二区| 亚洲精品美女久久久久99小说| 久久久久无码专区亚洲av| 久久久青草青青国产亚洲免观| 亚洲无线码一区二区三区| 精品亚洲永久免费精品| 亚洲人成电影在线天堂| 亚洲第一页在线观看| 亚洲专区中文字幕| 亚洲乱码国产乱码精华| 久久亚洲AV成人无码国产最大| 亚洲成A∨人片天堂网无码| 国产亚洲美日韩AV中文字幕无码成人| 日韩一卡2卡3卡4卡新区亚洲| 亚洲国产精品乱码一区二区| 久久久久久久亚洲Av无码 | 国产亚洲精品成人AA片新蒲金 | 久久亚洲精品无码gv| 亚洲一区二区三区免费| 亚洲av无码一区二区三区乱子伦 | 亚洲一区二区三区91| 亚洲经典千人经典日产|