最全面的單例模式講解

      作爲一名程序員,或許誰都能說出一種或幾種設計模式,其中單利模式是最常見、最簡單的一種設計模式,但是我們或許對單例模式的瞭解不一定全面。今天我們就探討下單例模式。

【單例模式的定義】

     單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。

【單例模式的三個基本要點】 

      一是這個類只能有一個實例;二是它必須自行創建這個實例;三是它必須自行向整個系統提供這個實例。

【單例模式分類】

     單例模式分爲餓漢式和懶漢式。

        
// 餓漢模式
public final class Singleton {
    private static Singleton instance=new Singleton();// 自行創建實例
    private Singleton(){}// 構造函數
    public static Singleton getInstance(){// 通過該函數向整個系統提供實例
        return instance;
    }
}

       餓漢式通過static修飾成員變量instance,該變量會在類初始化的過程中被收集進類構造器的clinit方法中,在多線程場景下,JVM會保證只有一個線程執行該clinit方法。等到唯一的一次clinit方法執行完成,其他線程將不會再執行該clinit方法,也就是說,static修飾的成員變量instance,在多線程環境下只實例化一次,從而保證了該對象只有一個實例

    餓漢式單例模式有一個缺點,在類初始化階段就創建好了對象,如果一個框架的類都採用這種方式,那麼在類初始化階段就把所有類都創建了一個對象,而一些類我們是不常用的,那麼這些不常用的類也被實例化了,這樣只是白白的浪費內存。餓漢式雖然高效,但是浪費內存。


// 懶漢模式
public final class Singleton {
    private static Singleton instance= null;// 不實例化
    private Singleton(){}// 構造函數
    public static Singleton getInstance(){// 通過該函數向整個系統提供實例
        if(null == instance){// 當 instance 爲 null 時,則實例化對象,否則直接返回對象
            instance = new Singleton();// 實例化對象
        }
        return instance;// 返回已存在的對象
    }
}

       懶漢式單例模式是爲了避免直接在加載類時就創建對象,採用懶加載的方式,只有當系統使用到類對象時,纔會創建對象。這種方式在單線程是沒有問題,但是在多線程環境下,就會出現實例化多個對象的情況。

   當線程A執行到if判斷還沒有創建對象時,A線程cpu時間片用完了,這時B線程也執行到If判斷,發現還沒有對象,就new一個對象出來,這時A線程又有執行權,繼續執行下面代碼,就會也new一個對象。這種情況下,就會創建多個實例。

【懶漢式單例模式逐步升級】

     通過上面分析,這種懶漢式在多線程環境下可能會創建多個實例。那我們採用加同步鎖synchronized的方式,保證多線程情況下僅創建一個實例。

      


// 懶漢模式 + synchronized 同步鎖
public final class Singleton {
    private static Singleton instance= null;// 不實例化
    private Singleton(){}// 構造函數
    public static synchronized Singleton getInstance(){// 加同步鎖,通過該函數向整個系統提供實例
        if(null == instance){// 當 instance 爲 null 時,則實例化對象,否則直接返回對象
            instance = new Singleton();// 實例化對象
        }
        return instance;// 返回已存在的對象
    }
}

       我們知道,同步鎖會增加鎖競爭,帶來系統性能開銷,從而導致系統性能下降,因此這種方式也會降低單例模式的性能。
還有,每次請求獲取類對象時,都會通過 getInstance() 方法獲取,除了第一次爲 null,其它每次請求基本都是不爲 null 的。在沒有加同步鎖之前,是因爲 if 判斷條件爲 null 時,才導致創建了多個實例。基於以上兩點,我們可以考慮將同步鎖放在 if 條件裏面,這樣就可以減少同步鎖資源競爭。所以我們進一步優化代碼如下:

    


// 懶漢模式 + synchronized 同步鎖
public final class Singleton {
    private static Singleton instance= null;// 不實例化
    private Singleton(){}// 構造函數
    public static Singleton getInstance(){// 加同步鎖,通過該函數向整個系統提供實例
        if(null == instance){// 當 instance 爲 null 時,則實例化對象,否則直接返回對象
          synchronized (Singleton.class){
              instance = new Singleton();// 實例化對象
          } 
        }
        return instance;// 返回已存在的對象
    }
}

       看到這裏,你是不是覺得這樣就可以了呢?答案是依然會創建多個實例。這是因爲當多個線程進入到 if 判斷條件裏,雖然有同步鎖,但是進入到判斷條件裏面的線程依然會依次獲取到鎖創建對象,然後再釋放同步鎖。所以我們還需要在同步鎖裏面再加一個判斷條件,進一步優化代碼如下:


// 懶漢模式 + synchronized 同步鎖 + double-check
public final class Singleton {
    private static Singleton instance= null;// 不實例化
    private Singleton(){}// 構造函數
    public static Singleton getInstance(){// 加同步鎖,通過該函數向整個系統提供實例
        if(null == instance){// 第一次判斷,當 instance 爲 null 時,則實例化對象,否則直接返回對象
          synchronized (Singleton.class){// 同步鎖
             if(null == instance){// 第二次判斷
                instance = new Singleton();// 實例化對象
             }
          } 
        }
        return instance;// 返回已存在的對象
    }
}

        以上這種方式,通常被稱爲 Double-Check,它可以大大提高支持多線程的懶漢模式的運行性能。那這樣做是不是就能保證萬無一失了呢?還會有什麼問題嗎?
      其實這裏又跟 Happens-Before 規則和重排序扯上關係了,這裏我們先來簡單瞭解下 Happens-Before 規則和重排序。
JVM編譯器爲了儘可能地減少寄存器的讀取、存儲次數,會充分複用寄存器的存儲值,比如以下代碼,如果沒有進行重排序優化,正常的執行順序是步驟 1/2/3,而在編譯期間進行了重排序優化之後,執行的步驟有可能就變成了步驟 1/3/2,這樣就能減少一次寄存器的存取次數。
int a = 1;// 步驟 1:加載 a 變量的內存地址到寄存器中,加載 1 到寄存器中,CPU 通過 mov 指令把 1 寫入到寄存器指定的內存中
int b = 2;// 步驟 2 加載 b 變量的內存地址到寄存器中,加載 2 到寄存器中,CPU 通過 mov 指令把 2 寫入到寄存器指定的內存中
a = a + 1;// 步驟 3 重新加載 a 變量的內存地址到寄存器中,加載 1 到寄存器中,CPU 通過 mov 指令把 1 寫入到寄存器指定的內存中
         在  JMM  中,重排序是十分重要的一環,特別是在併發編程中。如果 JVM 可以對它們進行任意排序以提高程序性能,也可能會給併發編程帶來一系列的問題。例如,我上面講到的 Double-Check 的單例問題,假設類中有其它的屬性也需要實例化,這個時候,除了要實例化單例類本身,還需要對其它屬性也進行實例化:

   


// 懶漢模式 + synchronized 同步鎖 + double-check
public final class Singleton {
    private static Singleton instance= null;// 不實例化
    public List<String> list = null;//list 屬性
    private Singleton(){
      list = new ArrayList<String>();
    }// 構造函數
    public static Singleton getInstance(){// 加同步鎖,通過該函數向整個系統提供實例
        if(null == instance){// 第一次判斷,當 instance 爲 null 時,則實例化對象,否則直接返回對象
          synchronized (Singleton.class){// 同步鎖
             if(null == instance){// 第二次判斷
                instance = new Singleton();// 實例化對象
             }
          } 
        }
        return instance;// 返回已存在的對象
    }
}

      在執行 instance = new Singleton(); 代碼時,正常情況下,實例過程這樣的:
        1、給 Singleton 分配內存;
        2、調用 Singleton 的構造函數來初始化成員變量;
        3、將 Singleton 對象指向分配的內存空間(執行完這步 singleton 就爲非 null 了)。
    如果虛擬機發生了重排序優化,這個時候步驟 3 可能發生在步驟 2 之前。如果初始化線程剛好完成步驟 3,而步驟 2 沒有進行時,則剛好有另一個線程到了第一次判斷,這個時候判斷爲非 null,並返回對象使用,這個時候實際沒有完成其它屬性的構造,因此使用這個屬性就很可能會導致異常。在這裏,Synchronized 只能保證可見性、原子性,無法保證執行的順序。

     這個時候,就體現出 Happens-Before 規則的重要性了。通過字面意思,你可能會誤以爲是前一個操作發生在後一個操作之前。然而真正的意思是,前一個操作的結果可以被後續的操作獲取。這條規則規範了編譯器對程序的重排序優化。
        我們知道 volatile 關鍵字可以保證線程間變量的可見性,簡單地說就是當線程 A 對變量 X 進行修改後,在線程 A 後面執行的其它線程就能看到變量 X 的變動。除此之外,volatile 在 JDK1.5 之後還有一個作用就是阻止局部重排序的發生,也就是說,volatile 變量的操作指令都不會被重排序。所以使用 volatile 修飾 instance 之後,Double-Check 懶漢單例模式就萬無一失了。

   第一種真正懶漢式單例模式代碼如下:

   

/**
 * 單例模式--懶漢式:synchronized + double-check + volatile
 * @author: lsl
 * @date: 2020/1/3
 */
public class Singlnton {
    private volatile static Singlnton instance = null;
    private List<String> list = null;
    
    private Singlnton(){
        list = new ArrayList<>();
    }
    
    public  static Singlnton getSinglnton(){
        if (null == instance){
            synchronized(Singlnton.class){
                if (null == instance){
                    instance = new Singlnton();
                }
            }
        }
        return instance;
    }
}

      這種方式實現的懶漢式單例模式雖然保證了只創建一個對象實例,但是由於加了synchronized同步鎖,性能會有一定降低,另外代碼實現也必將複雜。那麼有沒有其他方式實現懶漢式單例模式呢?答案是肯定有的,採用內部類來實現單例。代碼如下:

   

/**
 * 單例模式 - 懶漢式 : 內部類實現
 * @author: lsl
 * @date: 2020/1/3
 */
public class Singlnton {
    private Singlnton(){}

    public static class InnerSingleton{
        private static Singlnton instance = new Singlnton();
    }

    public static Singlnton getInstance(){
        return InnerSingleton.instance;
    }
}

      除了內部類的形式實現懶漢式單例模式,還可以通過枚舉實現懶漢式單例模式。代碼如下:

    

public class SinletonExample5 {
    private static SinletonExample5 instance = null;

    // 私有構造函數
    private SinletonExample5(){
    }

    public static SinletonExample5 getInstance(){
        return Sinleton.SINLETON.getInstance();
    }

    private enum Sinleton{
        SINLETON;

        private SinletonExample5 singleton;

        // JVM保證這個方法只調用一次
        Sinleton(){
            singleton = new SinletonExample5();
        }

        public SinletonExample5 getInstance(){
            return singleton;
        }
    }
}

 

發佈了62 篇原創文章 · 獲贊 28 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章