關於單例模式的最全總結

單例類是最簡單的一個OOP設計模式,然而單例模式並不那麼簡單,因此技術面試中面試官往往會問到它。
這裏總結了關於java中單例模式的一系列問題。僅供總結和分享,請不要用來難爲面試的娃們。

1. 1最簡單的單例--餓漢單例

餓漢單例指的是在類加載時完成單例對象實例化。下面是一個簡餓漢單例的示例:
package Singletons;

public class GreedySingleton{
	//private static
	private static GreedySingleton instance = new GreedySingleton();
	
	//private
	private GreedySingleton(){}
	
	//static
	public static GreedySingleton getInstance(){
		return instance;
	}
}


簡單得沒啥可說,只有一點需要注意:該單例的實現中getInstance()方法可以併發訪問,不需要再添加synchronized之類的同步
其實關於餓漢單例還有一個學習中無法想到的問題,這個問題只有真正使用時纔可能遇到,具體見下一節。

1.2 餓漢單例也不簡單

話不多說,直接上碼:
public class GreedySingleton_{
	//A:
	private static GreedySingleton_ instance = new GreedySingleton_();
	//B:
	private static int[] wallet;
	//C:
	static{
		wallet = new int [2];
		wallet[0] = 1;
	}
	//D:
	private GreedySingleton_(){
		if(wallet == null){
			wallet = new int[2];
		}
		wallet[1] = 2;
	}
	
	public static GreedySingleton_ getInstance(){
		return instance;
	}
	
	public void openUrWallet(){
		System.out.println("["+wallet[0]+","+wallet[1]+"]");
	}
	
	public static void main(String[] args){
		GreedySingleton_.getInstance().openUrWallet();
	}
}
輸出結果:
[1,0]
你可能猜對了,也可能分析錯了,這都不重要,重要的是代碼真正的執行情況:
程序從main方法開始,
首先遇到類GreedySingleton_,於是加載該類:
類加載經過class加載、驗證、解析、準備,然後進入初始化階段,執行<clinit>方法:
clinit方法首先執行 private static GreedySingleton_ instance = new GreedySingleton_():
這裏是一個new指令,於是先判斷GreedySingleton_是否已經加載,由於類加載工作除了初始化過程外其他過程均已完成,判定該類已經加載
於是分配一段堆內存來存放GreedySingleton_實例對象,並執行<init>方法:
構造方法外沒有非靜態屬性的初始化和賦值操作,所以這裏<init>的任務就是執行構造方法GreedySingleton_():
這是wallet還處於null狀態,因此執行wallet = new int[2]; wallet[1] = 2;
 從<init>退出,將<init>中分配內存的引用賦給instance屬性
        繼續執行<clinit>的後續代碼:wallet = new int [2]; wallet[0] = 1; 這時wallet被指向另一個數組對象,其內容爲[1,0] (之所以第二個未賦值的wallet[1]==0,原因是數組也是一個類,類實例化時new指令能保證分配到的是一段全零內存)
以上是代碼執行過程。

從以上分析可以看到,<init>方法在<clinit>方法執行過程中被執行,除非非常瞭解JVM規範,否則無法知道代碼真正的含義。
正常來講,應該先執行<clinit>方法,等類加載過程全部完成,纔可以執行<init>方法,否則只會帶來麻煩而不會達到任何好處。
爲了保證始終<clinit>先於<init>,可以遵循以下幾個原則:
0. 儘量避免自身類型的類屬性
1. 儘量避免在<clinit>涵蓋代碼部分中實例化自身引用
2. 儘量將自身類型實例化操作置於<clinit>涵蓋代碼的最後部分

2. 複雜點的單例--懶漢單例

懶漢單例就是等首次使用時才創建對象實例:
package Singletons;

public class LazySingleton {
	private static LazySingleton instance = null;
	
	private LazySingleton(){}
	
	//同步是爲了防止instance屬性未實例化時同時來了n個線程,他們同時到達代碼:if(instance==null)
	//都看到instance未創建,於是每人創建了一個出來
	public synchronized LazySingleton getInstance(){
		if(instance==null)
			instance = new LazySingleton();
		return instance;
	}
}

但是有一個很大的問題:每次訪問都要進行同步操作,大部分時間都花在同步上了。解決辦法就是引入Double-Check。
《Java與模式》一書提到Java語言無法實現Double-Check(原因是由於指令重排導致實例化完成與屬性的引用賦值先後無法預測,導致有些線程得到未初始化完成的單例對象)。但是早在JDK1.5對volatile進行修復之後,這個問題就已經解決:
package Singletons;

public class LazySingleton_ {
	//volatile
	private volatile static LazySingleton_ instance = null;
	
	private LazySingleton_(){}
	
	public static LazySingleton_ getInstance(){
		//Double-Check:
		if(instance == null){
			synchronized (LazySingleton_.class) {
				if(instance == null)
					instance = new LazySingleton_();
			}
		}
		
		return instance;
	}
}

volatile阻止了對instance的操作進程指令重排,同時保證了其可視性

3. 真正的單例--安全單例

前面的單例均通過構造方法私有化來保證"單"的要求。
所謂安全單例,指不能通過反射得到其第二個實例。
安全單例可以通過內嵌類、抽象類兩種方式來實現。
其中,內嵌類的方式得到的同時也是懶漢單例,是一個延遲加載的安全的單例,所以最爲推薦。
詳見我的另一篇博客http://blog.csdn.net/pbooodq/article/details/49125355

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章