天天看點

設計模式----一文讓你了解單例模式

單例模式的概念

確定一個類隻有一個執行個體【1】【2】,且自行執行個體化并向整個系統提供這個執行個體【3】

  1. 確定一個類隻有一個執行個體:那麼就要求這個類不能被外部執行個體化,也就是要求構造方法一定不能是公開的,是以構造方法隻能private
  2. 一個類隻有一個執行個體,這個執行個體是屬于目前類,即這個執行個體時目前類的類成員變量,即靜态變量,使用static
  3. 向外提供執行個體,需要我們可以提供一個靜态方法向外界提供目前類的執行個體,這個執行個體隻能在類内部進行執行個體化,不能夠放到外面去

    最簡單的單例實作模式如下

class Singleton{
    //靜态成員變量
    private static Singleton singleton;
    //私有構造方法
    private Singleton(){}
    
    //對外的靜态方法
    public static Singleton getInstance(){
        //執行個體暫時為空,根據執行個體化時間不一樣,有不同類型
        return null;
    }
}
           

單例的應用場景

單例模式隻允許建立一個對象,是以節約記憶體,可以加快對象通路速度,是以适合在對象需要被公用的場景下使用,如多個子產品使用同一個資料源連接配接對象等場景。

  1. 資源共享的情況下,避免由于資源操作做時導緻的性能或損耗等。如上述中的日志檔案,應用配置
  2. 控制資源的情況下,友善資源之間的互相通信。如線程池等

常見的如序列号生成器、網站計數器,還有建立一個對象需要消耗很多資源的話,如通路資料庫,也适合使用單例模式來減少資源的消耗

餓漢式

在類加載的時候就進行執行個體化,線程安全,調用效率高,但是不能延遲加載

class Singleton{
    //靜态成員變量,在類加載的時候就進行執行個體化
    private static Singleton singleton = new Singleton();
    //私有構造方法
    private Singleton(){}

    //對外的靜态方法
    public static Singleton getInstance(){
        return singleton;
    }
}
           

懶漢式

在第一次使用的時候進行執行個體化,線程安全,調用效率不高,但是可以延遲加載

class Singleton{
    //靜态成員變量
    private static Singleton singleton ;
    //私有構造方法
    private Singleton(){}

    //對外的靜态方法,這裡需要加鎖防止被多次執行個體化,在多線程中,如果AB線程都判空後,都會進行執行個體化,不加鎖就會被執行個體化兩次
    public synchronized static Singleton getInstance(){
        //如果使用時為空,進行執行個體化
        if(singleton==null){
            singleton = new Singleton();
        }
        return singleton;
    }
}
           

DCL懶漢式

雙重校驗鎖模式,由于JVM底層内部模型原因,偶爾會出問題,不建議使用(在代碼中給出了問題所在和解決方案)

為什麼需要兩次校驗對象是否為空?

在普通懶漢式中,我們可以看到synchronized鎖的是整個方法,但是執行個體化隻會發生在第一次,一旦被執行個體化後,就不會再被執行個體化了,是以我們可以減小同步的範圍:即在第一次判斷時,我們就加鎖以便進行執行個體化

第一次校驗:也就是第一個if(singleton==null),這個是為了代碼提高代碼執行效率,由于單例模式隻要一次建立執行個體即可,是以當建立了一個執行個體之後,再次調用getInstance方法就不必要進入同步代碼塊,不用競争鎖。直接傳回前面建立的執行個體即可。

第二次校驗:也就是第二個if(singleton == null),這個校驗是防止二次建立執行個體,假如有一種情況,當singleton還未被建立時,線程t1調用getInstance方法,由于第一次判斷singleton==null,此時線程t1準備繼續執行,但是由于資源被線程t2搶占了,此時t2頁調用getInstance方法,同樣的,由于singleton并沒有執行個體化,t2同樣可以通過第一個if,然後繼續往下執行,同步代碼塊,第二個if也通過,然後t2線程建立了一個執行個體singleton。此時t2線程完成任務,資源又回到t1線程,t1此時也進入同步代碼塊,如果沒有這個第二個if,那麼,t1就也會建立一個singleton執行個體,那麼,就會出現建立多個執行個體的情況,但是加上第二個if,就可以完全避免這個多線程導緻多次建立執行個體的問題
class Singleton{
    //靜态成員變量
    private volatile static Singleton singleton ;
    //私有構造方法
    private Singleton(){}

    //對外的靜态方法
    public static Singleton getInstance(){
        //如果使用時為空,進行執行個體化。隻需要建立一次,第二次對象存在了,就可以提高效率跳過鎖
        if(singleton==null){
            synchronized(Singleton.class){
                //多線程判斷,如果t1線程進入内層判斷,空的話進行初始化。此時t2也進來了,如果沒有内層判空,會對singleton進行了二次初始化
                if(singleton==null){
                    //由于cpu存在指令重排,如果t1執行了配置設定記憶體和執行位址這兩步操作,而沒有進行初始化對象,導緻t2認為對象已經完成,導緻錯誤,可以用volatile修飾變量,防止指令重排,這就是DCL可能會出現的問題和解決方案
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

           

附:

雙重校驗鎖進行初始化的執行個體必須使用volatile關鍵字來修飾防止指令重排

什麼叫指令重排:

singleton = new Singleton();這一步可以分為三步

  1. 配置設定記憶體
  2. 初始化對象
  3. 指向剛配置設定的位址

    由于cpu可能會優化步驟,若發生重排,如線程A隻想了1和3,還沒有執行2,B線程已經執行到判斷是否為null了,判斷不成立,B線程就會直接傳回未進行初始化的instance,導緻錯誤

靜态内部類

線程安全,調用效率高,可以延遲加載,類似餓漢式

public class Singleton4 {
     
    /**
     * 1、私有化構造器
     */
    private Singleton(){}
    /**
     * 2、聲明一個靜态内部類,在靜态内部類内部提供一個外部類的執行個體(常量,不可改變)
     * 初始化Singleton 的時候不會初始化SingletonClassInstance,實作了延時加載。并且線程安全
     
     * 當外部類被加載的時候,并不會建立SingletonClassInstance執行個體對象
     */
    private static class SingletonClassInstance{
        //該執行個體隻讀,不管誰都不能修改
        private static final Singleton instance = new Singleton();
    }
    /**
     * 3、對外提供一個擷取執行個體的方法:直接傳回靜态内部類中的那個常量執行個體
     * 調用的時候沒有同步等待,是以效率也高
     
     * 隻有調用這個方法時,靜态内部類才被加載,這個時候才會建立Instance
     * @return
     */
    public static Singleton getInstance(){
        return SingletonClassInstance.instance;
    }
 
}
           

枚舉

線程安全,調用效率高,但不能延遲加載

/**
 * 枚舉實作單例模式(枚舉本身就是單例)
 */
public enum Singleton {
    /**
     * 定義一個枚舉元素,它就是一個單例的執行個體了。
     */
    INSTANCE;
     
    /**
     * 對枚舉的一些操作
     */
    public void singletonOperation(){
         
    }
     
}
           

注意

單例模式一般來說是全局唯一的,但是在下面兩種情況下,也會出現多個執行個體

  1. 在分布式系統中,有多個JVM,每個JVM各有自己一個執行個體
  2. 一個JVM,使用了多個類加載器加載這個類,産生多個執行個體

如何破解單例

  1. 通過反射進行破解(不包含枚舉,因為枚舉本身是單例的,由jvm進行管理)
  2. 通過反序列化破解

    可以參考:https://www.cnblogs.com/shangxinfeng/p/6754345.html

五個方式如何選用

枚舉好于餓漢式,靜态内部類好于懶漢式

本文參考:

https://www.cnblogs.com/aeon/p/10212065.html

https://www.cnblogs.com/meet/p/5116398.html