日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

单例模式的优与劣

發布時間:2023/12/3 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 单例模式的优与劣 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

轉載自?大話設計模式(四)單例模式的優與劣

前言

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

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

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

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

public static Singleton GetInstance(){ if(singleton == null){singleton = newSingleton();}return singleton; } 如果在一開始調用 GetInstance()時,是由兩個線程同時調用的(這種情況是很常見的),注意是同時,(或者是一個線程進入 if 判斷語句后但還沒有實例化 Singleton 時,第二個線程到達,此時 singleton 還是為 null)這樣的話,兩個線程均會進入 GetInstance(),而后由于是第一次調用 GetInstance(),所以存儲在 Singleton 中的靜態變量 singleton 為 null ,這樣的話,就會讓兩個線程均通過 if 語句的條件判斷,然后調用 new Singleton()了,這樣的話,問題就出來了,因為有兩個線程,所以會創建兩個實例,

很顯然,這便違法了單例模式的初衷了,

那么如何解決上面出現的這個問題(即多線程下使用單例模式時有可能會創建多個實例這一現象)呢?

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

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

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

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

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

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

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

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

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

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

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

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

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

單例的三種實現方式    

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

懶漢式單例

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

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

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

private static volatile TestSingleton instance = null;public static Singleton GetInstance(){if(singleton == null){lock (syncObject)// synchronized (TestSingleton.class){if(singleton == null){singleton = newSingleton();}}}return singleton;}

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

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

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

餓漢式單例

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

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

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

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

登記式單例類(可忽略)

importjava.util.HashMap;importjava.util.Map;//登記式單例類.//類似Spring里面的方法,將類名注冊,下次從里面直接獲取。public class Singleton3 {private static Map<string,singleton3> map = newHashMap<string,singleton3>();static{Singleton3 single = newSingleton3();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直接返回,對于沒有登記的,則先登記,然后返回。
這里我對登記式單例標記了可忽略,我的理解來說,首先它用的比較少,另外其實內部實現還是用的餓漢式單例,因為其中的static方法塊,它的單例在類被裝載的時候就被實例化了。

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

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

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

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

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

餓漢式與懶漢式的區別

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

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

餓漢式在類創建的同時就實例化一個靜態對象出來,不管之后會不會使用這個單例,都會占據一定的內存,但是相應的,在第一次調用時速度也會更快,因為其資源已經初始化完成。

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

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

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

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

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

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

1.1單例對象的初始化

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

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

1.2單例對象的屬性更新

通常,為了實現配置信息的實時更新,會有一個線程不停檢測配置文件或配置數據庫的內容,一旦發現變化,就更新到單例對象的屬性中。在更新這些信息的時候,很可能還會有其他線程正在讀取這些信息,造成意想不到的后果。還是以通過單例對象屬性停止線程服務為例,如果更新屬性時讀寫不同步,可能訪問該屬性時這個屬性正好為空(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 = newGlobalConfig();}}public static GlobalConfig getInstance() {if(instance == null) {syncInit();}return instance;}public Vector getProperties() {return properties;}}

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

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

代碼如下:

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 = newGlobalConfig();}}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 datasyncReadOut();return properties;} }

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

?

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 = newGlobalConfig();}}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 objectGlobalConfig shadow = newGlobalConfig();properties = shadow.getProperties();}}

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

全局變量和單例模式的區別

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

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

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

單例模式的優與劣

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

主要優點

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

2、由于在系統內存中只存在一個對象,因此可以節約系統資源,對于一些需要頻繁創建和銷毀的對象,單例模式無疑可以提高系統的性能。

3、允許可變數目的實例。

主要缺點

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

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

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

公司面試中,“觀察者模式”也會被經常問到及寫出代碼,下篇博文將會分析講解。


總結

以上是生活随笔為你收集整理的单例模式的优与劣的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。