單例模式大家並不陌生,也都知道它分為什麼懶漢式、餓漢式之類的。但是你對單例模式的理解足夠透徹嗎?今天我帶大家一起來看看我眼中的單例,可能會跟你的認識有所不同。
下面是一個簡單的小執行個體:
//簡單懶漢式public class Singleton {//單例執行個體變數private static Singleton instance = null;//私人化的構造方法,保證外部的類不能通過構造器來執行個體化private Singleton() {}//擷取單例對象執行個體public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } System.out.println("我是簡單懶漢式單例!"); return instance; }}
很容易看出,上面這段代碼在多線程的情況下是不安全的,當兩個線程進入if (instance == null)時,兩個線程都判斷instance為空白,接下來就會得到兩個執行個體了。這不是我們想要的單例。
接下來我們用加鎖的方式來實現互斥,從而保證單例的實現。
//同步法懶漢式public class Singleton {//單例執行個體變數private static Singleton instance = null;//私人化的構造方法,保證外部的類不能通過構造器來執行個體化private Singleton() {}//擷取單例對象執行個體public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } System.out.println("我是同步法懶漢式單例!"); return instance; }}
加上synchronized後確實保證了安全執行緒,但是這樣就是最好的方法嗎?很顯然它不是,因為這樣一來每次調用getInstance()方法是都會被加鎖,而我們只需要在第一次調用getInstance()的時候加鎖就可以了。這顯然影響了我們程式的效能。我們繼續尋找更好的方法。
經過分析發現,只需要保證instance = new Singleton()是線程互斥就可以保證安全執行緒,所以就有了下面這個版本:
//雙重鎖定懶漢式public class Singleton {//單例執行個體變數private static Singleton instance = null;//私人化的構造方法,保證外部的類不能通過構造器來執行個體化private Singleton() {}//擷取單例對象執行個體public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } System.out.println("我是雙重鎖定懶漢式單例!"); return instance; }}
這次看起來既解決了安全執行緒問題,又不至於每次調用getInstance()都會加鎖導致降低效能。看起來是一個完美的解決方案,事實上是這樣的嗎?
很遺憾,事實並非我們想的那麼完美。java平台記憶體模型中有一個叫“無序寫”(out-of-order writes)的機制。正是這個機制導致了雙重檢查加鎖方法的失效。這個問題的關鍵在上面代碼上的第5行:instance = new Singleton(); 這行其實做了兩個事情:1、調用構造方法,建立了一個執行個體。2、把這個執行個體賦值給instance這個執行個體變數。可問題就是,這兩步jvm是不保證順序的。也就是說。可能在調用構造方法之前,instance已經被設定為非空了。下面我們一起來分析一下:
假設有兩個線程A、B
1、線程A進入getInstance()方法。
2、因為此時instance為空白,所以線程A進入synchronized塊。
3、線程A執行 instance = new Singleton(); 把執行個體變數instance設定成了非空。(注意,是在調用構造方法之前。)
4、線程A退出,線程B進入。
5、線程B檢查instance是否為空白,此時不為空白(第三步的時候被線程A設定成了非空)。線程B返回instance的引用。(問題出現了,這時instance的引用並不是Singleton的執行個體,因為沒有調用構造方法。)
6、線程B退出,線程A進入。
7、線程A繼續調用構造方法,完成instance的初始化,再返回。
難道就沒有一個好方法了嗎?好的方法肯定是有的,我們繼續探索!
//解決無序寫問題懶漢式public class Singleton {//單例執行個體變數private static Singleton instance = null;//私人化的構造方法,保證外部的類不能通過構造器來執行個體化private Singleton() {}//擷取單例對象執行個體public static Singleton getInstance() {if (instance == null) { synchronized (Singleton.class) { //1Singleton temp = instance; //2if (temp == null) {synchronized (Singleton.class) { //3 temp = new Singleton(); //4 }instance = temp; //5 }}}System.out.println("我是解決無序寫懶漢式單例!");return instance;}}
1、線程A進入getInstance()方法。
2、因為instance是空的 ,所以線程A進入位置//1的第一個synchronized塊。
3、線程A執行位置//2的代碼,把instance賦值給本地變數temp。instance為空白,所以temp也為空白。
4、因為temp為空白,所以線程A進入位置//3的第二個synchronized塊。(後來想想這個鎖有點多餘)
5、線程A執行位置//4的代碼,把temp設定成非空,但還沒有調用構造方法!(“無序寫”問題)
6、如果線程A阻塞,線程B進入getInstance()方法。
7、因為instance為空白,所以線程B試圖進入第一個synchronized塊。但由於線程A已經在裡面了。所以無法進入。線程B阻塞。
8、線程A啟用,繼續執行位置//4的代碼。調用構造方法。產生執行個體。
9、將temp的執行個體引用賦值給instance。退出兩個synchronized塊。返回執行個體。
10、線程B啟用,進入第一個synchronized塊。
11、線程B執行位置//2的代碼,把instance執行個體賦值給temp本地變數。
12、線程B判斷本地變數temp不為空白,所以跳過if塊。返回instance執行個體。
到此為止,上面的問題我們是解決了,但是我們突然發現為瞭解決安全執行緒問題,但給人的感覺就像身上纏了很多毛線.... 亂糟糟的,所以我們要精簡一下:
//餓漢式public class Singleton {//單例變數 ,static的,在類載入時進行初始化一次,保證安全執行緒 private static Singleton instance = new Singleton(); //私人化的構造方法,保證外部的類不能通過構造器來執行個體化。 private Singleton() {}//擷取單例對象執行個體 public static Singleton getInstance() { System.out.println("我是餓漢式單例!"); return instance;}}
看到上面的代碼,瞬間覺得這個世界清靜了。不過這種方式採用的是餓漢式的方法,就是預先聲明Singleton對象,這樣帶來的一個缺點就是:如果構造的單例很大,構造完又遲遲不使用,會導致資源浪費。
到底有沒有完美的方法呢?繼續看:
//內部類實現懶漢式public class Singleton { private static class SingletonHolder{ //單例變數 private static Singleton instance = new Singleton(); } //私人化的構造方法,保證外部的類不能通過構造器來執行個體化。 private Singleton() { } //擷取單例對象執行個體 public static Singleton getInstance() { System.out.println("我是內部類單例!"); return SingletonHolder.instance; }}
懶漢式(避免上面的資源浪費)、安全執行緒、代碼簡單。因為java機制規定,內部類SingletonHolder只有在getInstance()方法第一次調用的時候才會被載入(實現了lazy),而且其載入過程是安全執行緒的(實現安全執行緒)。內部類載入的時候執行個體化一次instance。
簡單說一下上面提到的無序寫,這是jvm的特性,比如聲明兩個變數,String a; String b; jvm可能先載入a也可能先載入b。同理,instance = new Singleton();可能在調用Singleton的建構函式之前就把instance置成了非空。這是很多人會有疑問,說還沒有執行個體化出Singleton的一個對象,那麼instance怎麼就變成非空了呢?它的值現在是什麼呢?想瞭解這個問題就要明白instance
= new Singleton();這句話是怎麼執行的,下面用一段虛擬碼向大家解釋一下:
mem = allocate(); //為Singleton對象分配記憶體。instance = mem; //注意現在instance是非空的,但是還沒有被初始化。ctorSingleton(instance); //調用Singleton的建構函式,傳遞instance.
由此可見當一個線程執行到instance = mem; 時instance已為非空,如果此時另一個線程進入程式判斷instance為非空,那麼直接就跳轉到return instance;而此時Singleton的構造方法還未調用instance,現在的值為allocate();返回的記憶體對象。所以第二個線程得到的不是Singleton的一個對象,而是一個記憶體對象。
以上就是就是我對單例模式的一點小小的思考跟理解,熱烈歡迎各位大神前來指導批評。