輕松手寫單例模式的6種實現方式!再也不怕面試官問了!


手撕單例模式不管是筆試還是面試,都是高頻題了。
今天就來說一下單例模式的原理和 6 種實現方式。

一、單例模式的定義

定義: 確保一個類只有一個實例,並提供該實例的全局訪問點。

這樣做的好處是:有些實例,全局只需要一個就夠了,使用單例模式就可以避免一個全局使用的類,頻繁的創建與銷毀,耗費系統資源。

二、單例模式的設計要素

  • 一個私有構造函數 (確保只能單例類自己創建實例)
  • 一個私有靜態變量 (確保只有一個實例)
  • 一個公有靜態函數 (給使用者提供調用方法)

簡單來說就是,單例類的構造方法不讓其他人修改和使用;並且單例類自己只創建一個實例,這個實例,其他人也無法修改和直接使用;然后單例類提供一個調用方法,想用這個實例,只能調用。這樣就確保了全局只創建了一次實例。

三、單例模式的6種實現及各實現的優缺點

(一)懶漢式(線程不安全)

實現:

public class Singleton {
     private static Singleton uniqueInstance;
   
     private Singleton() {
        
    }
    
    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

說明: 先不創建實例,當第一次被調用時,再創建實例,所以被稱為懶漢式。

優點: 延遲了實例化,如果不需要使用該類,就不會被實例化,節約了系統資源。

缺點: 線程不安全,多線程環境下,如果多個線程同時進入了 if (uniqueInstance == null) ,若此時還未實例化,也就是uniqueInstance == null,那么就會有多個線程執行 uniqueInstance = new Singleton(); ,就會實例化多個實例;

(二)餓漢式(線程安全)

實現:

public class Singleton {
    
    private static Singleton uniqueInstance = new Singleton();
    
    private Singleton() {
    }
    
    public static Singleton getUniqueInstance() {
        return uniqueInstance;
    }
    
}

說明: 先不管需不需要使用這個實例,直接先實例化好實例 (餓死鬼一樣,所以稱為餓漢式),然后當需要使用的時候,直接調方法就可以使用了。

優點: 提前實例化好了一個實例,避免了線程不安全問題的出現。

缺點: 直接實例化好了實例,不再延遲實例化;若系統沒有使用這個實例,或者系統運行很久之后才需要使用這個實例,都會操作系統的資源浪費。

(三)懶漢式(線程安全)

實現:

public class Singleton {
    private static Singleton uniqueInstance;
    
    private static singleton() {
    }
   
    private static synchronized Singleton getUinqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
    
}

說明: 實現和 線程不安全的懶漢式 幾乎一樣,唯一不同的點是,在get方法上 加了一把 鎖。如此一來,多個線程訪問,每次只有拿到鎖的的線程能夠進入該方法,避免了多線程不安全問題的出現。

優點: 延遲實例化,節約了資源,並且是線程安全的。

缺點: 雖然解決了線程安全問題,但是性能降低了。因為,即使實例已經實例化了,既后續不會再出現線程安全問題了,但是鎖還在,每次還是只能拿到鎖的線程進入該方法,會使線程阻塞,等待時間過長。

(四)雙重檢查鎖實現(線程安全)

實現:

public class Singleton {
    
    private volatile static Singleton uniqueInstance;
    
    private Singleton() {
    }
    
    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }  
}

說明: 雙重檢查數相當於是改進了 線程安全的懶漢式。線程安全的懶漢式 的缺點是性能降低了,造成的原因是因為即使實例已經實例化,依然每次都會有鎖。而現在,我們將鎖的位置變了,並且多加了一個檢查。 也就是,先判斷實例是否已經存在,若已經存在了,則不會執行判斷方法內的有鎖方法了。 而如果,還沒有實例化的時候,多個線程進去了,也沒有事,因為里面的方法有鎖,只會讓一個線程進入最內層方法並實例化實例。如此一來,最多最多,也就是第一次實例化的時候,會有線程阻塞的情況,后續便不會再有線程阻塞的問題。

為什么使用 volatile 關鍵字修飾了 uniqueInstance 實例變量 ?

uniqueInstance = new Singleton(); 這段代碼執行時分為三步:

  1. 為 uniqueInstance 分配內存空間
  2. 初始化 uniqueInstance
  3. 將 uniqueInstance 指向分配的內存地址

正常的執行順序當然是 1>2>3 ,但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1>3>2。
單線程環境時,指令重排並沒有什么問題;多線程環境時,會導致有些線程可能會獲取到還沒初始化的實例。
例如:線程A 只執行了 1 和 3 ,此時線程B來調用 getUniqueInstance(),發現 uniqueInstance 不為空,便獲取 uniqueInstance 實例,但是其實此時的 uniqueInstance 還沒有初始化。

解決辦法就是加一個 volatile 關鍵字修飾 uniqueInstance ,volatile 會禁止 JVM 的指令重排,就可以保證多線程環境下的安全運行。

優點: 延遲實例化,節約了資源;線程安全;並且相對於 線程安全的懶漢式,性能提高了。

缺點: volatile 關鍵字,對性能也有一些影響。

(五)靜態內部類實現(線程安全)

實現:

public class Singleton {
    
    private Singleton() {
    }
    
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
     
}

說明: 首先,當外部類 Singleton 被加載時,靜態內部類 SingletonHolder 並沒有被加載進內存。當調用 getUniqueInstance() 方法時,會運行 return SingletonHolder.INSTANCE; ,觸發了 SingletonHolder.INSTANCE ,此時靜態內部類 SingletonHolder 才會被加載進內存,並且初始化 INSTANCE 實例,而且 JVM 會確保 INSTANCE 只被實例化一次。

優點: 延遲實例化,節約了資源;且線程安全;性能也提高了。

(六)枚舉類實現(線程安全)

實現:

public enum Singleton {
    
    INSTANCE;

    //添加自己需要的操作
    public void doSomeThing() {
    
    }
    
}

說明: 默認枚舉實例的創建就是線程安全的,且在任何情況下都是單例。

優點: 寫法簡單,線程安全,天然防止反射和反序列化調用。

  • 防止反序列化
    序列化:把java對象轉換為字節序列的過程;
    反序列化: 通過這些字節序列在內存中新建java對象的過程;
    說明: 反序列化 將一個單例實例對象寫到磁盤再讀回來,從而獲得了一個新的實例。
    我們要防止反序列化,避免得到多個實例。
    枚舉類天然防止反序列化。
    其他單例模式 可以通過 重寫 readResolve() 方法,從而防止反序列化,使實例唯一重寫 readResolve() :
private Object readResolve() throws ObjectStreamException{
        return singleton;
}

四、單例模式的應用場景

應用場景舉例:

  • 網站計數器。
  • 應用程序的日志應用。
  • Web項目中的配置對象的讀取。
  • 數據庫連接池。
  • 多線程池。
  • ......

使用場景總結:

  • 頻繁實例化然后又銷毀的對象,使用單例模式可以提高性能。
  • 經常使用的對象,但實例化時耗費時間或者資源多,如數據庫連接池,使用單例模式,可以提高性能,降低資源損壞。
  • 使用線程池之類的控制資源時,使用單例模式,可以方便資源之間的通信。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM