單例模式

單實例的正確寫法

並文章屬於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程序或者開發一個產品時,這種模式還是很重要的。綜上所述,我個人比較推薦寫法五和寫法一,寫法七怎麼看着也彆扭。

另外感謝大家的討論,這個話題先到這兒吧,我寫本文章的主要目的是爲了糾正寫法三的錯誤。不知道你的項目中是否還存在寫法三的代碼呢?

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