1. 程式人生 > >單例模式的幾種寫法(包含雙檢鎖寫法)

單例模式的幾種寫法(包含雙檢鎖寫法)

餓漢式單例類

Java程式碼 複製程式碼
  1. publicclass Singleton   
  2. {   
  3. private Singleton(){   
  4.     }   
  5. privatestatic Singleton instance = new Singleton();   
  6. privatestatic Singleton getInstance(){   
  7. return instance;   
  8.     }   
  9. }  
public class Singleton
{
	private Singleton(){
	
	}

	private static Singleton instance = new Singleton();

	private static Singleton getInstance(){
		return instance;
	}
}

 餓漢式提前例項化,沒有懶漢式中多執行緒問題,但不管我們是不是呼叫getInstance()都會存在一個例項在記憶體中

內部類式單例類

Java程式碼 複製程式碼
  1. publicclass Singleton      
  2. {         
  3. private Singleton(){      
  4.     }      
  5. privateclass SingletonHoledr(){      
  6. privatestatic Singleton instance = new Singleton();      
  7.     }      
  8. privatestatic Singleton getInstance(){      
  9. return SingletonHoledr.instance;      
  10.     }      
  11. }   
public class Singleton   
{      
        private Singleton(){   
       
    }   
  
    private class SingletonHoledr(){   
        private static Singleton instance = new Singleton();   
    }   
  
    private static Singleton getInstance(){   
        return SingletonHoledr.instance;   
    }   
} 

內部類式中,實現了延遲載入,只有我們呼叫了getInstance(),才會建立唯一的例項到記憶體中.並且也解決了懶漢式中多執行緒的問題.解決的方式是利用了Classloader的特性.

懶漢式單例類

Java程式碼 複製程式碼
  1. publicclass Singleton      
  2. {         
  3. private Singleton(){      
  4.     }      
  5. privatestatic Singleton instance;      
  6. publicstatic Singleton getInstance(){      
  7. if(instance == null){      
  8. return instance = new Singleton();      
  9.         }else{      
  10. return instance;      
  11.         }      
  12.     }      
  13. }    
public class Singleton   
{      
    private Singleton(){   
  
    }   
  
    private static Singleton instance;   
    public static Singleton getInstance(){   
        if(instance == null){   
            return instance = new Singleton();   
        }else{   
            return instance;   
        }   
    }   
}  

在懶漢式中,有執行緒A和B,當執行緒A執行到第8行時,跳到執行緒B,當B也執行到8行時,兩個執行緒的instance都為空,這樣就會生成兩個例項。解決的辦法是同步:

可以同步但是效率不高:

Java程式碼 複製程式碼
  1. publicclass Singleton      
  2. {         
  3. private Singleton(){      
  4.     }      
  5. privatestatic Singleton instance;      
  6. publicstaticsynchronized Singleton getInstance(){      
  7. if(instance == null){      
  8. return instance = new Singleton();      
  9.         }else{      
  10. return instance;      
  11.         }      
  12.     }      
  13. }    
public class Singleton   
{      
    private Singleton(){   
  
    }   
  
    private static Singleton instance;   
    public static synchronized Singleton getInstance(){   
        if(instance == null){   
            return instance = new Singleton();   
        }else{   
            return instance;   
        }   
    }   
}  

這樣寫程式不會出錯,因為整個getInstance是一個整體的"critical section",但就是效率很不好,因為我們的目的其實只是在第一個初始化instance的時候需要locking(加鎖),而後面取用instance的時候,根本不需要執行緒同步。
於是聰明的人們想出了下面的做法:

雙檢鎖寫法: 

Java程式碼 複製程式碼
  1. publicclass Singleton{   
  2. privatestatic Singleton single;    //宣告靜態的單例物件的變數
  3. private Singleton(){}    //私有構造方法 
  4. publicstatic Singleton getSingle(){    //外部通過此方法可以獲取物件  
  5. if(single == null){      
  6. synchronized (Singleton.class) {   //保證了同一時間只能只能有一個物件訪問此同步塊      
  7. if(single == null){       
  8.                 single = new Singleton();           
  9.         }      
  10.       }   
  11.     }     
  12. return single;   //返回建立好的物件 
  13.   }   
  14. }  
public class Singleton{
  private static Singleton single;    //宣告靜態的單例物件的變數
  private Singleton(){}    //私有構造方法 
  
  public static Singleton getSingle(){    //外部通過此方法可以獲取物件  
    if(single == null){   
        synchronized (Singleton.class) {   //保證了同一時間只能只能有一個物件訪問此同步塊      
            if(single == null){    
                single = new Singleton();        
        }   
      }
    }  
    return single;   //返回建立好的物件 
  }
}

思路很簡單,就是我們只需要同步(synchronize)初始化instance的那部分程式碼從而使程式碼既正確又很有效率。
這就是所謂的“雙檢鎖”機制(顧名思義)。
很可惜,這樣的寫法在很多平臺和優化編譯器上是錯誤的。

原因在於:instance = new Singleton()這行程式碼在不同編譯器上的行為是無法預知的。一個優化編譯器可以合法地如下實現instance = new Singleton():

1. instance  = 給新的實體分配記憶體

2. 呼叫Singleton的建構函式來初始化instance的成員變數

現在想象一下有執行緒A和B在呼叫getInstance,執行緒A先進入,在執行到步驟1的時候被踢出了cpu。然後執行緒B進入,B看到的是instance  已經不是null了(記憶體已經分配),
於是它開始放心地使用instance,但這個是錯誤的,因為在這一時刻,instance的成員變數還都是預設值,A還沒有來得及執行步驟2來完成instance的初始化。

當然編譯器也可以這樣實現:

1. temp = 分配記憶體

2. 呼叫temp的建構函式

3. instance = temp

如果編譯器的行為是這樣的話我們似乎就沒有問題了,但事實卻不是那麼簡單,因為我們無法知道某個編譯器具體是怎麼做的,因為在Java的memory model裡對這個問題沒有定義。

雙檢鎖對於基礎型別(比如int)適用。很顯然吧,因為基礎型別沒有呼叫建構函式這一步。