設計模式之懶漢式單例模式

設計模式之單例模式

定義

保證一個類僅有一個實例,並提供一個全局訪問點。java

類型

建立型設計模式

適用場景

想確保任何狀況下都絕對只有一個實例。安全

優勢

  • 在內存中只有一個實例,減小內存開銷。特別是一個對象在使用時須要頻繁建立和銷燬同時建立和銷燬性能沒法優化時。
  • 能夠避免對資源的多重佔用。好比我在對一個文件進行寫操做,使用單例能夠避免同時對這個文件進行寫操做。
  • 設置全局訪問點,嚴格控制訪問。

缺點

沒有接口,拓展困難。若是想要拓展就必需要修改代碼。多線程

下面開始看代碼。咱們首先實現如下懶漢式。懶漢式的單例模式注重的是延時加載,只有在引用的時候纔會加載。jvm

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
 
    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}    
複製代碼

這個方式寫的在單線程下是沒有什麼問題的。 可是在多線程的環境就會出現問題,假設其中一個線程到 lazySingleton = new LazySingleton();這一行時尚未return出去。 此時又有一個線程進入這個方法。 由於此時尚未return出去。 因此在進行判斷時lazySingleton依舊爲null。 咱們說 單例模式的優勢就是:ide

在內存中只有一個實例,減小內存開銷。特別是一個對象在使用時須要頻繁建立和銷燬同時建立和銷燬性能沒法優化時。性能

原本咱們但願這個對象之建立一次。這樣的話這個對象2次進入都建立了對象,這樣就不能達到減少內存開銷的目的。 小夥伴們能夠用這段代碼debug走一下看看(使用idea在多線程環境下debug記得要切換一下,不會的小夥伴能夠百度一下)。優化

咱們看一下這個代碼該怎麼優化。那位很簡單嘛,不就是多線程的問題嗎,加個鎖不就好了。好咱們加上鎖了。idea

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
 
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}  

複製代碼

如今是咱們在方法上添加了synchronized關鍵字。這個方法是個靜態方法也是說這個所至關於加載類上面。一樣的咱們還能夠這樣寫,效果是同樣的。spa

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
 
    public static LazySingleton getInstance(){
    synchronized(LazySingleton.class){
         if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
    }
        return lazySingleton;
    }
} 

複製代碼

這作確實解決了多線程的問題,可是咱們都知道,synchronized是比較耗費資源的加鎖方式,並且在使用static方法時鎖的是這個class。鎖的範圍也是很是的大, 性能消耗也是很是明顯。下面咱們看看有沒有在性能和安全性上可以取得平衡的方案。

咱們可使用doubleCheck雙重檢查的方式來上實現懶漢式單例。這種方式是兼顧性能和安全的一種實現方式。

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

複製代碼

雙重鎖如今再也不是對方法進行加鎖,咱們如今對代碼塊進行加鎖。此時咱們是鎖定了這個類,這個if方法仍是能夠進來,這種方式大大的縮短了synchronized鎖定的範圍,從而減小性能消耗。 至於這裏爲何要加兩個if判斷,同窗們對比上面的第二種在類上面加鎖的代碼,就很明顯能夠看出了。 而後這種實現方式還有個坑。咱們來解決一下。 咱們來看這個代碼, lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();看起來是一行代碼,其實這裏經歷了三個過程:

  1. 分配內存給這個對象
  2. 初始化對象
  3. 設置lazyDoubleCheckSingleton 指向剛分配的內存地址

這裏的坑就是2和3操做可能會被重排序,2和3的操做順序可能會被顛倒。此時執行流程爲:

  1. 分配內存給這個對象

. 3. 設置lazyDoubleCheckSingleton 指向剛分配的內存地址

. 2. 初始化對象

此時在執行到3操做是,其實咱們的實例尚未完成實例化的操做,可是在進行空判斷的時候,由於已經被分配了內存空間,判斷時並不爲空,而後直接return一個空對象。若是咱們拿到這個對象進行操做就會報空指針異常了。那爲何會進行重排序呢,在單線程中java語言規範中容許2和3進行重排序,由於重排序能夠提升程序的執行效率。

爲了方便你們理解,你們能夠看下面的圖,這是一個單線程的執行過程。

對於在多線程中的執行就變成下面的方式了。

如圖,對於線程0步驟2和步驟3進行重排序並不會影響最後的結果。可是對於線程1就不同了。當線程0執行步驟3以後線程1進入對象是否爲空的判斷,此時對象並無完成初始化,可是對象已經被設置了內存空間,因此判斷是否爲空時不爲空,這時候線程1就會執行return操做也就是步驟4,然而拿到的是一個沒有初始化的完成的對象。

那麼咱們該如何解決這個問題呢? 我能夠不容許線程0進行2和3的重排序,或者不讓線程1看到2和3的重排序。

咱們使用volatile這個關鍵字限制線程0進行重排序。完整的代碼以下:

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();

                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}
複製代碼

下面咱們來實現一下不讓線程看到操做2和操做3的重排序的方案。 使用靜態內部類的方式。

public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
     private StaticInnerClassSingleton(){
    }
}
複製代碼

咱們來講一下爲何這樣作可讓操做2和操做3對線程1不可見。

類在初始化的階段:也就是類在加載而且在線程使用以前,jvm在類的初始化階段會獲取一個類初始化的一個鎖,這個鎖會同步多個線程對一個類的初始化。示意圖以下,當線程0在執行時,對於線程1是不會看到的。

上面的方式咱們稱之爲基於類初始化的延遲加載的單例模式。 根據java語言規範主要如下幾種種狀況,發生這個類將被當即初始化。

  1. 有個實例被建立
  2. 類中的靜態方法被調用
  3. 類中的靜態成員被賦值
  4. 類中的靜態成員被使用

懶漢式的單例模式咱們就降到這裏

相關文章
相關標籤/搜索