java中單例模式的實現

實現單例

單例模式是:確保一個類只有一個實例,並提供全局唯一訪問點。
唯一訪問點需要把構造方法私有,提供獲取實例的公共方法
全局都是一個實例:實例的對象必須是static

基於以上幾點,可以寫出基本懶漢式和餓漢式

一、餓漢式
public class FirstSingle {
//餓漢式  類加載就創建了對象
private FirstSingle() {
}
private static FirstSingle single = new FirstSingle();

public static FirstSingle getSingle() {
	return single;
}
}

二、懶漢式 延時使用時創建對象
public class FirstSingle {
//懶漢式  延時使用時創建對象
private FirstSingle() {
	
}
private static FirstSingle single = null;

public static FirstSingle getSingle() {
	//保持外部獲取時,只創建對象一次 ,使用同一個對象。若不判斷,每次外部獲取都會新建對象,與以前的不是同一個對象,違背了類只有一個實例原則!
	if(single == null)
		single = new FirstSingle();
	return single;
	//賴漢侷限:多線程下if語句判斷不安全
}
}

三、 懶漢式多線程下優化 -> 雙重校驗式

①既然上述的懶漢式是線程不安全的,那就加鎖使之變成線程安全

public class FirstSingle {
//既然上述的懶漢式是線程不安全的,那就加鎖使之變成線程安全
private FirstSingle() {
	
}
private static FirstSingle single = null;

public synchronized static FirstSingle getSingle() {
	if(single == null)
		single = new FirstSingle();
	return single;
	//加同步賴漢侷限:多線程下某一時間點只能一個線程訪問,其他線程等待被阻塞時間過長,即使其他線程中的single已經被實例了,效率堪憂
}
}

加同步賴漢侷限:多線程下某一時間點只能一個線程訪問,其他線程等待被阻塞時間過長,即使其他線程中的single已經被實例了,效率堪憂

②進一步優化,既然上面有線程以及獲取了single實例後的對象,所以根本不需要再進入if判斷中,因此可以將同步鎖的範圍由整個方法縮小至if處

public class FirstSingle {
//進一步優化,既然上面有線程以及獲取了single實例後的對象,所以根本不需要再進入if判斷中,因此可以將同步鎖的範圍由整個方法縮小至if處
private FirstSingle() {
	
}
private static FirstSingle single = null;

public  static FirstSingle getSingle() {
	
	if(single == null)
		synchronized(FirstSingle.class) {
			//不過這時又出現新的問題。假設兩個線程T1、T2都是沒有得到single的實例。
			//1.T1先進入同步塊後,T2進入if在同步塊前等待了。
			//2.T1完成實例化,退出同步塊釋放同步鎖,return返回
			//3.此刻因T2已經老早進入if中,不知道single已經被實例化,所以T2也進入同步塊又實例化一遍,返回。這樣造成兩次getSingle的並不是統一對象!
			//其根本原因是T2進入同步塊中沒有在一次判斷是否single已經實例過!
		single = new FirstSingle();
		}
	return single;
}
}

不過這時又出現新的問題。假設兩個線程T1、T2都是沒有得到single的實例。
1.T1先進入同步塊後,T2進入if在同步塊前等待了。
2.T1完成實例化,退出同步塊釋放同步鎖,return返回
3.此刻因T2已經老早進入if中,不知道single已經被實例化,所以T2也進入同步塊又實例化一遍,返回。這樣造成兩次getSingle的並不是統一對象!
其根本原因是T2進入同步塊中沒有在一次判斷是否single已經實例過!

③既然上面分析 T2進入同步塊,沒有判斷上次退出同步其他線程創建了實例。那加上判斷是否可行

public class FirstSingle {
//既然上面分析 T2進入同步塊,沒有判斷上次退出同步其他線程創建了實例。那加上判斷是否可行
private FirstSingle() {
	
}
private static FirstSingle single = null;

public  static FirstSingle getSingle() {
	
	if(single == null)
		synchronized(FirstSingle.class) {
			if(single == null)
				//這裏加上if是解決了上面的問題,
				//但是還有考慮JVM指令重排的特性,低概率的同步錯誤出現,多線程下可能會有某線程得到沒有初始化的實例。
				//single = new FirstSingle(); 會有三步過程:
					//1.爲new FirstSingle()實例對象申請空間,對象的字段設置爲默認值 0值
					//2.初始化實例對象
					//3.single指向實例的對象
				//指令重排後,1—>3—>2, T1 完成1,3 ,釋放同步鎖,T1還未完成2,沒有return;T2進入同步塊發現single不是null,返回single這個引用,但是2實例會、化對象還沒有完成,出現了錯誤。
				//若T1在T2返回前完成了第2步,那T2返回也就是正確的對象。
				
				single = new FirstSingle();
		}
	return single;
}
}

④既然有指令重排,那就不讓他指令重排,jdk提供volatile關鍵字。終極版如下 雙重校驗鎖式

public class FirstSingle {
//既然有指令重排,那就不讓他指令重排,jdk提供volatile關鍵字。終極版如下
private FirstSingle() {
	
}
// volatile 保證每個線程得到singlon值是最新的,可見性、防止jvm優化指令重排
private static volatile FirstSingle single = null;

public  static FirstSingle getSingle() {
	//避免線程已經得到實例都進入同步塊
	if(single == null)
		synchronized(FirstSingle.class) {
			//第二個if 對於多個沒有實例化的線程,避免在進入同步塊之前其他線程已經實例過,防止重複實例
			if(single == null)
				//new 這句 加上volatile防止jvm優化指令重排,通知其他線程single的可見,避免出現上面不加volatile時的同步錯誤
				single = new FirstSingle();
		}
	return single;
}
}

四、靜態內部類

利用私有的靜態內部類防止外界訪問且,因靜態全部外部類對象只有一份內部類,內部類用以創建對象,符合了單例的全局只有一個對象。再上提供公共方法獲取實例單例,符合提供全局的訪問點。

優勢:外部類加載時,內部類不會加載,內部不加載就不new,只有在內部類被引用,即調用getInstance時導致JVM加載內部類
優勢:線程安全性,和延遲加載單例的實例化

package designpatten.singleton;

//靜態內部類方式 實現單例
public class SingletonInnerClass {

private SingletonInnerClass() {}
//私有靜態內部類 防止外界訪問且全部外部類對象只有一份內部類,

private static class SingletonHolder {
	 static SingletonInnerClass single = new SingletonInnerClass(); 
}

public SingletonInnerClass getInstance() {
	return SingletonHolder.single;
}
}
//保證了單例的唯一性:在第一次創建單例後,該外部類的在方法去的常量池已經存在,再下一次getInstance會把single的符號引用直接變成直接引用去找常量池的單例
//所以在除第一初始單例外,其他調用getInstance會直接返回單例對象,這裏像是餓漢式

//線程安全性:jvm會保證在進行類加載時<clinit>會加載類的static部分,當然這裏如加載外部類不會主動加載靜態內部類,
//因爲jvm有且僅有的5中情況對類初始化中沒有對靜態內部類初始,所以它是被動初始的.
//jvm保證一個線程在初始化類期間是線程安全的,也就說其他線程若要初始同一個會被阻塞。這點保證了在線程調用getInstance初始內部類是線程安全

	類加載時機:JAVA虛擬機在有且僅有的5種場景下會對類進行初始化。
1.遇到new、getstatic、setstatic或者invokestatic這4個字節碼指令時,對應的java代碼場景爲:new一個關鍵字或者一個實例化對象時、讀取或設置一個靜態字段時(final修飾、已在編譯期把結果放入常量池的除外)、調用一個類的靜態方法時。
2.使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒進行初始化,需要先調用其初始化方法進行初始化。
3.當初始化一個類時,如果其父類還未進行初始化,會先觸發其父類的初始化。
4.當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的類),虛擬機會先初始化這個類。
5.當使用JDK 1.7等動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

這5種情況被稱爲是類的主動引用,

虛擬機會保證一個類的()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的()方法,其他線程都需要阻塞等待,直到活動線程執行()方法完畢。

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