單實例的正確寫法
並文章屬於Java併發編程實戰中例子。但結合實際場景進行了闡述。
通常,我們如果寫一個單實例模式的對象,一般會這樣寫:
寫法一:
public class Singleton {
private static final Singleton instance = new Singleton();
/**
* 防止其他人new對象
*/
private Singleton(){
System.out.println("init");
}
public static Singleton getInstance(){
return instance;
}
}
這種方式叫飢餓式單實例,意思是說,不管你用不用這個類的方法,我都把這個類需要的一切資源都分配好。但這樣寫有一個問題,就是如果這類需要的資源比較多,在系統啓動的時候,就會很慢。
因此要求有懶漢式單實例,於是就出現了第二中寫法,
寫法二:
public class Singleton {
private static Singleton instance = null;
/**
* 防止其他人new對象
*/
private Singleton(){
System.out.println("init");
}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
這種方式叫懶漢式單實例,即通常所說的延遲加載。這樣,在系統啓動的時候,不會加載類所需要的各種資源,只有真正使用的時候纔去加載各種資源。
但這種方法馬上就可以看出問題,因爲在多線程情況下,可能會導致重複初始化的問題(不明白這個道理,那您需要補充一下同步及多線程知識了)。於是有了改進版,即目前網上比較流行的寫法。
寫法三:
public class Singleton {
private static Singleton instance = null;
/**
* 防止其他人new對象
*/
private Singleton(){
System.out.println("init");
}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
加上關鍵字synchronized,可以保證只有一個線程在執行這個方法。這個方法至此應該說是比較完美的了,但是,專家不這麼認爲,在高併發多線程的訪問系統中,synchronized關鍵字會讓程序的吞吐量急劇下降,因此,在高併發系統中,應該儘量避免使用synchronized鎖。
但這並不能難住我們聰明的軟件工程師,有人便寫出了雙重鎖的程序。方法如下:
寫法四:
public class Singleton {
private static Singleton instance = null;
/**
* 防止其他人new對象
*/
private Singleton(){
System.out.println("init");
}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
這樣,通常獲得單實例引用是沒有鎖的,只有第一次初始化時纔會加鎖,而且如果多個線程進入臨界區區後,理論上只有第一個進入臨界區的線程纔會初始化對象,之後進入臨界區的線程因爲之前的線程已經初始化,就不會再次進行初始化。
但專家怎麼說呢?這個代碼有問題。首先,這個程序對同步的應用很到位,即當進入synchronied區,只有一個線程在訪問Singleton類。但卻忽略了變量的可見性。因爲在沒有同步的保護下,instance的值在多個線程中可能都是空的,因爲即便第一個線程對類進行了初始化,並把類的引用賦值給了instance變量,但也不能保證instance變量的值對其他線程是可見的,因爲變量instance沒有采用同步的機制。
在java5之後,可以在instance前面添加volatile關鍵字來解決這個問題,但是這種雙重鎖的方式已經不建議使用。
那麼,看看大師推薦的寫法吧,見 Java Concurrency In Practice的List 16.6代碼:
寫法五:
public class Singleton {
private static class SingletonHolder {
public static Singleton resource = new Singleton();
}
public static Singleton getResource() {
return SingletonHolder.resource ;
}
private Singleton(){
}
}
綜上各種寫法,發現寫法一雖然在啓動時會讓系統啓動的慢一些,但卻不失爲一種簡潔而高效的寫法,當然,如果確實對系統啓動時的速度要求高的話,則應該考慮寫法五了。
另外,其實單實例方法還有好多種,在effective Java中有寫到:
寫法六:
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton(){}
public void method(){
//...
}
public static void main(String[] a){
//調用方法。
Singleton.INSTANCE.method();
}
}
寫法七:
/**
* 利用枚舉巧妙創建單實例
*/
public enum Singleton {
INSTANCE;
public void method(){
//...
}
public static void main(String[] a){
//調用方法。
Singleton.INSTANCE.method();
}
}
另外,雙重鎖的方式,在加上volatile關鍵字後,也是高效安全的寫法。
寫法八:
public class Singleton {
private static volatile Singleton instance = null;
/**
* 防止其他人new對象
*/
private Singleton(){
System.out.println("init");
}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
其實,在今天spring大行其道的天下,單實例需求已經不多,spring中的bean默認都是單實例。但是要做一些app程序或者開發一個產品時,這種模式還是很重要的。綜上所述,我個人比較推薦寫法五和寫法一,寫法七怎麼看着也彆扭。
另外感謝大家的討論,這個話題先到這兒吧,我寫本文章的主要目的是爲了糾正寫法三的錯誤。不知道你的項目中是否還存在寫法三的代碼呢?