/**
* 單例模式-雙重校驗鎖
* @author szekinwin
*
*/
public class SingleTon3 {
private SingleTon3(){}; //私有化構造方法
private static volatile SingleTon3 singleTon=null;
public static SingleTon3 getInstance(){
//第一次校驗
if(singleTon==null){
synchronized(SingleTon3.class){
//第二次校驗
if(singleTon==null){
singleTon=new SingleTon3();
}
}
}
return singleTon;
}
public static void main(String[]args){
for(int i=0;i<200;i++){
new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+":"+SingleTon3.getInstance().hashCode());
}
}).start();
}
}
}
注意事項:
問題:為什么需要兩次判斷if(singleTon==null)?
分析:第一次校驗:由於單例模式只需要創建一次實例,如果后面再次調用getInstance方法時,則直接返回之前創建的實例,因此大部分時間不需要執行同步方法里面的代碼,大大提高了性能。如果不加第一次校驗的話,那跟上面的懶漢模式沒什么區別,每次都要去競爭鎖。
第二次校驗:如果沒有第二次校驗,假設線程t1執行了第一次校驗后,判斷為null,這時t2也獲取了CPU執行權,也執行了第一次校驗,判斷也為null。接下來t2獲得鎖,創建實例。這時t1又獲得CPU執行權,由於之前已經進行了第一次校驗,結果為null(不會再次判斷),獲得鎖后,直接創建實例。結果就會導致創建多個實例。所以需要在同步代碼里面進行第二次校驗,如果實例為空,則進行創建。
需要注意的是,private static volatile SingleTon3 singleTon=null;需要加volatile關鍵字,否則會出現錯誤。問題的原因在於JVM指令重排優化的存在。在某個線程創建單例對象時,在構造方法被調用之前,就為該對象分配了內存空間並將對象的字段設置為默認值。此時就可以將分配的內存地址賦值給instance字段了,然而該對象可能還沒有初始化。若緊接着另外一個線程來調用getInstance,取到的就是狀態不正確的對象,程序就會出錯。
(4)靜態內部類:同樣也是利用了類的加載機制,它與餓漢模式不同的是,它是在內部類里面去創建對象實例。這樣的話,只要應用中不使用內部類,JVM就不會去加載這個單例類,也就不會創建單例對象,從而實現懶漢式的延遲加載。也就是說這種方式可以同時保證延遲加載和線程安全。