设计模式-单例模式(Singleton)
首先明確一個問題,在某些情況下,有些對象,我們只需要一個就可以了。比如,一臺計算機連接到多個打印機上,但是這個計算機上的打印程序只能有一個,因為可能會有多個打印作業(yè)同時輸出到打印機中,這里就可以通過單例模式來避免這個問題。
簡單來說,單例模式的作用就是保證在應用程序的生命周期中,任何一個時刻,單例類的實例只有一個(當然也可以不存在)。
單例模式的類圖:
?
?
從上面的類圖中可以看出,在單例類中有一個私有的構造函數 Singleton ,
?
有一個聲明為public的 GetInstance()的方法返回一個Singleton實例。
?
通過上面的類圖不難看出單例模式的特點,從而也可以給出
單例模式的定義:單例模式保證一個類僅有一個實例,同時這個類還必須提供一個訪問該類的全局訪問點。
首先來看一個典型的實現:
1 /** 2 * 3 * 基礎的單例模式,Lazy模式,非線程安全 4 * 優(yōu)點:lazy,初次使用時實例化單例,避免資源浪費 5 * 缺點:1、lazy,如果實例初始化非常耗時,初始使用時,可能造成性能問題 6 * 2、非線程安全。多線程下可能會有多個實例被初始化。 7 * 8 * @author Wahson Leung 9 * @version 2013-4-20 10 * 11 */ 12 public class Singleton { 13 /** 單例實例變量 */ 14 private static Singleton instance = null; 15 16 /** 17 * 私有化的構造方法,保證外部的類不能通過構造器來實例化。 18 */ 19 private Singleton() { 20 } 21 22 /* 23 * 獲取單例對象事例 24 * 25 * @return 單例對象 26 */ 27 public static Singleton getInstance() { 28 if (instance == null) { 29 instance = new Singleton(); 30 } 31 return instance; 32 } 33 }?
?
注釋中已經有簡單的分析了。接下來分析一下關于“非線程安全”的部分。
1、當線程A進入到第28行時,檢查instance是否為空,此時是空的。
2、此時,線程B也進入到28行。切換到線程B執(zhí)行。同樣檢查instance為空,于是往下執(zhí)行29行),創(chuàng)建了一個實例。接著返回了。
3、在切換回線程A,由于之前檢查到instance為空。所以也會執(zhí)行29行創(chuàng)建實例。返回。
4、至此,已經有兩個實例被創(chuàng)建了,這不是我們所希望的。?
?怎么解決線程安全問題?
方法一:同步方法。即在getInstance()方法上加上synchronized關鍵字。這時單例變成了
1 /** 2 * 同步方法 的單例模式,Lazy模式,線程安全 3 * 優(yōu)點:1、lazy,初次使用時實例化單例,避免資源浪費 2、線程安全 4 * 缺點:1、lazy,如果實例初始化非常耗時,初始使用時,可能造成性能問題 5 * 2、每次調用getInstance()都要獲得同步鎖,性能消耗。 6 * 7 * @author Wahson Leung 8 * @version 2013-4-20 9 * 10 */ 11 public class Singleton { 12 /** 單例實例變量 */ 13 private static Singleton instance = null; 14 15 /** 16 * 私有化的構造方法,保證外部的類不能通過構造器來實例化。 17 */ 18 private Singleton() { 19 } 20 21 /* 22 * 獲取單例對象事例 23 * 24 * 同步方法,實現線程互斥訪問,保證線程安全。 25 * 26 * @return 單例對象 27 */ 28 public static Singleton getInstance() { 29 if (instance == null) { 30 synchronized (Singleton.class) { 31 Singleton temp = instance; 32 if (temp == null) { 33 synchronized (Singleton.class) { 34 temp = new Singleton(); 35 } 36 instance = temp; 37 } 38 } 39 } 40 return instance; 41 } 42 }加上synchronized后確實實現了線程的互斥訪問getInstance()方法。從而保證了線程安全。但是這樣就完美了么?我們看。其實在典型實現里,會導致問題的只是當instance還沒有被實例化的時候,多個線程訪問第29行的代碼才會導致問題。而當instance已經實例化完成后。每次調用getInstance(),其實都是直接返回的。即使是多個線程訪問,也不會出問題。但給方法加上synchronized后。所有getInstance()的調用都要同步了。其實我們只是在第一次調用的時候要同步。而同步需要消耗性能。這就是問題。
方法二:雙重檢查加鎖Double-checked locking。
其實經過分析發(fā)現,我們只要保證?instance =?new?Singleton();?是線程互斥訪問的就可以保證線程安全了。那把同步方法加以改造,只用synchronized塊包裹這一句。就得到了下面的代碼:
但是經過分析發(fā)現這個方法是不行的!
原因是:
1、線程A和線程B同時進入//1的位置。這時instance是為空的。
2、線程A進入synchronized塊,創(chuàng)建實例,線程B等待。
3、線程A返回,線程B繼續(xù)進入synchronized塊,創(chuàng)建實例。。。
4、這時已經有兩個實例創(chuàng)建了。?
為了解決這個問題。我們需要在//2的之前,再加上一次檢查instance是否被實例化。(雙重檢查加鎖)接下來,代碼變成了這樣:
?
1 public static Singleton getInstance() { 2 if(instance==null){ 3 synchronized (Singleton.class) { 4 if (instance == null) { 5 instance = new Singleton(); 6 } 7 } 8 } 9 return instance; 10 }?
這樣,當線程A返回,線程B進入synchronized塊后,會先檢查一下instance實例是否被創(chuàng)建,這時實例已經被線程A創(chuàng)建過了。所以線程B不會再創(chuàng)建實例,而是直接返回。貌似!到此為止,這個問題已經被我們完美的解決了。遺憾的是,事實完全不是這樣!這個方法在單核和多核的cpu下都不能保證很好的工作。導致這個方法失敗的原因是當前java平臺的內存模型。java平臺內存模型中有一個叫“無序寫”(out-of-order writes)的機制。正是這個機制導致了雙重檢查加鎖方法的失效。這個問題的關鍵在上面代碼上的第5行:instance =?new?Singleton();?這行其實做了兩個事情:1、調用構造方法,創(chuàng)建了一個實例。2、初始化變量instance來引用這個實例。可問題就是,這兩步jvm是不保證順序的。也就是說。可能在調用構造方法之前,instance已經被設置為非空了。下面我們看一下出問題的過程:
1、線程A進入getInstance()方法。
2、因為此時instance為空,所以線程A進入synchronized塊。
3、線程A執(zhí)行?instance =?new?Singleton();?把實例變量instance設置成了非空。(注意,是在調用構造方法之前。)
4、線程A退出,線程B進入。
5、線程B檢查instance是否為空,此時不為空(第三步的時候被線程A設置成了非空)。線程B返回instance的引用。(問題出現了,這時instance的引用并不是Singleton的實例,因為沒有調用構造方法。)?
6、線程B退出,線程A進入。
7、線程A繼續(xù)調用構造方法,完成instance的初始化,再返回。?
好吧,繼續(xù)努力,解決由“無序寫”帶來的問題。
?
1 public static Singleton getInstance() { 2 if (instance == null) { 3 synchronized (Singleton.class) { //1 4 Singleton temp = instance; //2 5 if (temp == null) { 6 synchronized (Singleton.class) { //3 7 temp = new Singleton(); //4 8 } 9 instance = temp; //5 10 } 11 } 12 } 13 return instance; 14 }?
解釋一下執(zhí)行步驟:
1、線程A進入getInstance()方法。
2、因為instance是空的 ,所以線程A進入位置//1的第一個synchronized塊。
3、線程A執(zhí)行位置//2的代碼,把instance賦值給本地變量temp。instance為空,所以temp也為空。?
4、因為temp為空,所以線程A進入位置//3的第二個synchronized塊。
5、線程A執(zhí)行位置//4的代碼,把temp設置成非空,但還沒有調用構造方法!(“無序寫”問題)?
6、線程A阻塞,線程B進入getInstance()方法。
7、因為instance為空,所以線程B試圖進入第一個synchronized塊。但由于線程A已經在里面了。所以無法進入。線程B阻塞。
8、線程A激活,繼續(xù)執(zhí)行位置//4的代碼。調用構造方法。生成實例。
9、將temp的實例引用賦值給instance。退出兩個synchronized塊。返回實例。
10、線程B激活,進入第一個synchronized塊。
11、線程B執(zhí)行位置//2的代碼,把instance實例賦值給temp本地變量。
12、線程B判斷本地變量temp不為空,所以跳過if塊。返回instance實例。
好吧,問題終于解決了,線程安全了。但是我們的代碼由最初的3行代碼變成了現在的一大坨。于是又有了下面的方法。
方法三:預先初始化static變量。
?
1 /** 2 * 預先初始化static變量 的單例模式 非Lazy 線程安全 優(yōu)點: 1、線程安全 缺點: 3 * 1、非懶加載(餓漢式),如果構造的單例很大,構造完又遲遲不使用,會導致資源浪費。 4 * 5 * @author Wahson Leung 6 * @version 2013-4-20 7 * 8 */ 9 public class HungrySingleton { 10 /** 單例變量 ,static的,在類加載時進行初始化一次,保證線程安全 */ 11 private static HungrySingleton instance = new HungrySingleton(); 12 13 /** 14 * 私有化的構造方法,保證外部的類不能通過構造器來實例化。 15 */ 16 private HungrySingleton() { 17 } 18 19 /** 20 * 獲取單例對象實例 21 * 22 * @return 單例對象 23 */ 24 public static HungrySingleton getInstance() { 25 return instance; 26 } 27 }?
看到這個方法,世界又變得清凈了。由于java的機制,static的成員變量只在類加載的時候初始化一次,且類加載是線程安全的。所以這個方法實現的單例是線程安全的。但是這個方法卻犧牲了Lazy的特性。單例類加載的時候就實例化了。如注釋所述:非懶加載,如果構造的單例很大,構造完又遲遲不使用,會導致資源浪費。
那到底有沒有完美的辦法?懶加載,線程安全,代碼簡單。
方法四:使用內部類。
1 /** 2 * 基于內部類的單例模式 Lazy 線程安全 優(yōu)點: 1、線程安全 2、lazy 3 * 4 * @author Wahson Leung 5 * @version 2013-4-20 6 * 7 */ 8 public class InnerSingleton { 9 10 /** 11 * 內部類,用于實現lazy機制 12 */ 13 private static class SingletonHolder { 14 /** 單例變量 */ 15 private static InnerSingleton instance = new InnerSingleton(); 16 } 17 18 /** 19 * 私有化的構造方法,保證外部的類不能通過構造器來實例化。 20 */ 21 private InnerSingleton() { 22 } 23 24 /** 25 * 獲取單例對象實例 26 * 27 * @return 單例對象 28 */ 29 public static InnerSingleton getInstance() { 30 return SingletonHolder.instance; 31 } 32 }解釋一下,因為java機制規(guī)定,內部類SingletonHolder只有在getInstance()方法第一次調用的時候才會被加載(實現了lazy),而且其加載過程是線程安全的(實現線程安全)。內部類加載的時候實例化一次instance。
總結一下:
1、如果單例對象不大,允許非懶加載,可以使用方法三。
2、如果需要懶加載,且允許一部分性能損耗,可以使用方法一。(官方說目前高版本的synchronized已經比較快了)
3、如果需要懶加載,且不怕麻煩,可以使用方法二。
4、如果需要懶加載,沒有且!推薦使用方法四。?
?
轉載于:https://www.cnblogs.com/wahsonleung/archive/2013/04/20/3031908.html
總結
以上是生活随笔為你收集整理的设计模式-单例模式(Singleton)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Sketch 在 mac 运行缓慢怎么办
- 下一篇: 虚拟大师 安装Linux系统,VMOS