你真的會用單例模式?

来源:https://www.cnblogs.com/vandusty/archive/2019/09/02/11444293.html
-Advertisement-
Play Games

你寫的的單例模式多線程下是否安全?是否懶載入?如何防止反射破壞單例模式?閱讀本文,解鎖單例模式的更多細節。 ...


單例模式可以說只要是一個合格的開發都會寫,但是如果要深究,小小的單例模式可以牽扯到很多東西,比如:多線程是否安全?是否懶載入?性能等等。還有你知道幾種單例模式的寫法呢?如何防止反射破壞單例模式?

一、 單例模式

1.1 定義

單例模式就是在程式運行中只實例化一次,創建一個全局唯一對象。有點像 Java 的靜態變數,但是單例模式要優於靜態變數:

  1. 靜態變數在程式啟動的時候JVM就會進行載入,如果不使用,會造成大量的資源浪費;
  2. 單例模式能夠實現懶載入,能夠在使用實例的時候才去創建實例。

開發工具類庫中的很多工具類都應用了單例模式,比例線程池、緩存、日誌對象等,它們都只需要創建一個對象,如果創建多份實例,可能會帶來不可預知的問題,比如資源的浪費、結果處理不一致等問題。

1.2 單例的實現思路

  1. 靜態化實例對象;
  2. 私有化構造方法,禁止通過構造方法創建實例;
  3. 提供一個公共的靜態方法,用來返回唯一實例。

1.3 單例的好處

  1. 只有一個對象,記憶體開支少、性能好;
  2. 避免對資源的多重占用;
  3. 在系統設置全局訪問點,優化和共用資源訪問。

二、 單例模式的實現

  1. 餓漢模式
  2. 懶漢模式
  3. 雙重檢查鎖模式
  4. 靜態內部類單例模式
  5. 枚舉類實現單例模式

2.1 餓漢模式

在定義靜態屬性時,直接實例化了對象

public class HungryMode {

    /**
     * 利用靜態變數來存儲唯一實例
     */
    private static final HungryMode instance = new HungryMode();

    /**
     * 私有化構造函數
     */
    private HungryMode(){
        // 裡面可以有很多操作
    }

    /**
     * 提供公開獲取實例介面
     * @return
     */
    public static HungryMode getInstance(){
        return instance;
    }
}

2.1.1 優點

由於使用了static關鍵字,保證了在引用這個變數時,關於這個變數的所以寫入操作都完成,所以保證了JVM層面的線程安全

2.1.2 缺點

不能實現懶載入,造成空間浪費:如果一個類比較大,我們在初始化的時就載入了這個類,但是我們長時間沒有使用這個類,這就導致了記憶體空間的浪費。

所以,能不能只有用到 getInstance()方法,才會去初始化單例類,才會載入單例類中的數據。所以就有了:懶漢式

2.2 懶漢模式

懶漢模式是一種偷懶的模式,在程式初始化時不會創建實例,只有在使用實例的時候才會創建實例,所以懶漢模式解決了餓漢模式帶來的空間浪費問題。

2.2.1 懶漢模式的一般實現

public class LazyMode {
    /**
     * 定義靜態變數時,未初始化實例
     */
    private static LazyMode instance;

    /**
     * 私有化構造函數
     */
    private LazyMode(){
        // 裡面可以有很多操作
    }
    /**
     * 提供公開獲取實例介面
     * @return
     */
    public static LazyMode getInstance(){
        // 使用時,先判斷實例是否為空,如果實例為空,則實例化對象
        if (instance == null) {
            instance = new LazyMode();
        }
        return instance;
    }
}

但是這種實現在多線程的情況下是不安全的,有可能會出現多份實例的情況:

if (instance == null) {
    instance = new LazyMode();
}

假設有兩個線程同時進入到上面這段代碼,因為沒有任何資源保護措施,所以兩個線程可以同時判斷的 instance 都為空,都將去初始化實例,所以就會出現多份實例的情況。

2.2.2 懶漢模式的優化

我們給getInstance()方法加上synchronized關鍵字,使得getInstance()方法成為受保護的資源就能夠解決多份實例的問題。

public class LazyModeSynchronized {
    /**
     * 定義靜態變數時,未初始化實例
     */
    private static LazyModeSynchronized instance;
    /**
     * 私有化構造函數
     */
    private LazyModeSynchronized(){
        // 裡面可以有很多操作
    }
    /**
     * 提供公開獲取實例介面
     * @return
     */
    public synchronized static LazyModeSynchronized getInstance(){
        /**
         * 添加class類鎖,影響了性能,加鎖之後將代碼進行了串列化,
         * 我們的代碼塊絕大部分是讀操作,在讀操作的情況下,代碼線程是安全的
         *
         */
        if (instance == null) {
            instance = new LazyModeSynchronized();
        }
        return instance;
    }
}

2.2.3 懶漢模式的優點

實現了懶載入,節約了記憶體空間。

2.2.4 懶漢模式的缺點

  1. 在不加鎖的情況下,線程不安全,可能出現多份實例;
  2. 在加鎖的情況下,會使程式串列化,使系統有嚴重的性能問題。

懶漢模式中加鎖的問題,對於getInstance()方法來說,絕大部分的操作都是讀操作,讀操作是線程安全的,所以我們沒必讓每個線程必須持有鎖才能調用該方法,我們需要調整加鎖的問題。由此也產生了一種新的實現模式:雙重檢查鎖模式

2.3 雙重檢查鎖模式

2.3.1 雙重檢查鎖模式的一般實現

public class DoubleCheckLockMode {

    private static DoubleCheckLockMode instance;

    /**
     * 私有化構造函數
     */
    private DoubleCheckLockMode(){

    }
    /**
     * 提供公開獲取實例介面
     * @return
     */
    public static DoubleCheckLockMode getInstance(){
        // 第一次判斷,如果這裡為空,不進入搶鎖階段,直接返回實例
        if (instance == null) {
            synchronized (DoubleCheckLockMode.class) {
                // 搶到鎖之後再次判斷是否為空
                if (instance == null) {
                    instance = new DoubleCheckLockMode();
                }
            }
        }
        return instance;
    }
}

雙重檢查鎖模式解決了單例、性能、線程安全問題,但是這種寫法同樣存在問題:在多線程的情況下,可能會出現空指針問題,出現問題的原因是JVM在實例化對象的時候會進行優化和指令重排序操作。

2.3.2 什麼是指令重排?

private SingletonObject(){
      // 第一步
     int x = 10;
      // 第二步
     int y = 30;
     // 第三步
     Object o = new Object(); 
}

上面的構造函數SingletonObject()JVM 會對它進行指令重排序,所以執行順序可能會亂掉,但是不管是那種執行順序,JVM 最後都會保證所以實例都完成實例化。 如果構造函數中操作比較多時,為了提升效率,JVM 會在構造函數裡面的屬性未全部完成實例化時,就返回對象。雙重檢測鎖出現空指針問題的原因就是出現在這裡,當某個線程獲取鎖進行實例化時,其他線程就直接獲取實例使用,由於JVM指令重排序的原因,其他線程獲取的對象也許不是一個完整的對象,所以在使用實例的時候就會出現空指針異常問題

2.3.3 雙重檢查鎖模式優化

要解決雙重檢查鎖模式帶來空指針異常的問題,只需要使用volatile關鍵字,volatile關鍵字嚴格遵循happens-before原則,即:在讀操作前,寫操作必須全部完成。

public class DoubleCheckLockModelVolatile {
    /**
     * 添加volatile關鍵字,保證在讀操作前,寫操作必須全部完成
     */
    private static volatile DoubleCheckLockModelVolatile instance;
    /**
     * 私有化構造函數
     */
    private DoubleCheckLockModelVolatile(){

    }
    /**
     * 提供公開獲取實例介面
     * @return
     */
    public static DoubleCheckLockModelVolatile getInstance(){

        if (instance == null) {
            synchronized (DoubleCheckLockModelVolatile.class) {
                if (instance == null) {
                    instance = new DoubleCheckLockModelVolatile();
                }
            }
        }
        return instance;
    }
}

2.4 靜態內部類模式

靜態內部類模式也稱單例持有者模式,實例由內部類創建,由於 JVM 在載入外部類的過程中, 是不會載入靜態內部類的, 只有內部類的屬性/方法被調用時才會被載入, 並初始化其靜態屬性。靜態屬性由static修飾,保證只被實例化一次,並且嚴格保證實例化順序。

public class StaticInnerClassMode {

    private StaticInnerClassMode(){

    }

    /**
     * 單例持有者
     */
    private static class InstanceHolder{
        private  final static StaticInnerClassMode instance = new StaticInnerClassMode();

    }

    /**
     * 提供公開獲取實例介面
     * @return
     */
    public static StaticInnerClassMode getInstance(){
        // 調用內部類屬性
        return InstanceHolder.instance;
    }
}

這種方式跟餓漢式方式採用的機制類似,但又有不同。兩者都是採用了類裝載的機制來保證初始化實例時只有一個線程。不同的地方:

  1. 餓漢式方式是只要Singleton類被裝載就會實例化,沒有Lazy-Loading的作用;
  2. 靜態內部類方式在Singleton類被裝載時並不會立即實例化,而是在需要實例化時,調用getInstance()方法,才會裝載SingletonInstance類,從而完成Singleton的實例化。

類的靜態屬性只會在第一次載入類的時候初始化,所以在這裡,JVM幫助我們保證了線程的安全性,在類進行初始化時,別的線程是無法進入的。

所以這種方式在沒有加任何鎖的情況下,保證了多線程下的安全,並且沒有任何性能影響和空間的浪費

2.5 枚舉類實現單例模式

因為枚舉類型是線程安全的,並且只會裝載一次,設計者充分的利用了枚舉的這個特性來實現單例模式,枚舉的寫法非常簡單,而且枚舉類型是所用單例實現中唯一一種不會被破壞的單例實現模式

public class EnumerationMode {
    
    private EnumerationMode(){
        
    }

    /**
     * 枚舉類型是線程安全的,並且只會裝載一次
     */
    private enum Singleton{
        INSTANCE;

        private final EnumerationMode instance;

        Singleton(){
            instance = new EnumerationMode();
        }

        private EnumerationMode getInstance(){
            return instance;
        }
    }

    public static EnumerationMode getInstance(){

        return Singleton.INSTANCE.getInstance();
    }
}

適用場合:

  1. 需要頻繁的進行創建和銷毀的對象;
  2. 創建對象時耗時過多或耗費資源過多,但又經常用到的對象;
  3. 工具類對象;
  4. 頻繁訪問資料庫或文件的對象。

三、單例模式的問題及解決辦法

除枚舉方式外, 其他方法都會通過反射的方式破壞單例

3.1 單例模式的破壞

/**
 * 以靜態內部類實現為例
 * @throws Exception
 */
@Test
public void singletonTest() throws Exception {
    Constructor constructor = StaticInnerClassMode.class.getDeclaredConstructor();
    constructor.setAccessible(true);

    StaticInnerClassMode obj1 = StaticInnerClassMode.getInstance();
    StaticInnerClassMode obj2 = StaticInnerClassMode.getInstance();
    StaticInnerClassMode obj3 = (StaticInnerClassMode) constructor.newInstance();

    System.out.println("輸出結果為:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}

控制台列印:

輸出結果為:1454171136,1454171136,1195396074

從輸出的結果我們就可以看出obj1obj2為同一對象,obj3為新對象。obj3是我們通過反射機制,進而調用了私有的構造函數,然後產生了一個新的對象。

3.2 如何阻止單例破壞

可以在構造方法中進行判斷,若已有實例, 則阻止生成新的實例,解決辦法如下:

public class StaticInnerClassModeProtection {

    private static boolean flag = false;

    private StaticInnerClassModeProtection(){
        synchronized(StaticInnerClassModeProtection.class){
            if(flag == false){
                flag = true;
            }else {
                throw new RuntimeException("實例已經存在,請通過 getInstance()方法獲取!");
            }
        }
    }

    /**
     * 單例持有者
     */
    private static class InstanceHolder{
        private  final static StaticInnerClassModeProtection instance = new StaticInnerClassModeProtection();
    }

    /**
     * 提供公開獲取實例介面
     * @return
     */
    public static StaticInnerClassModeProtection getInstance(){
        // 調用內部類屬性
        return InstanceHolder.instance;
    }
}

測試:

/**
 * 在構造方法中進行判斷,若存在則拋出RuntimeException
 * @throws Exception
 */
@Test
public void destroyTest() throws Exception {
    Constructor constructor = StaticInnerClassModeProtection.class.getDeclaredConstructor();
    constructor.setAccessible(true);

    StaticInnerClassModeProtection obj1 = StaticInnerClassModeProtection.getInstance();
    StaticInnerClassModeProtection obj2 = StaticInnerClassModeProtection.getInstance();
    StaticInnerClassModeProtection obj3 = (StaticInnerClassModeProtection) constructor.newInstance();

    System.out.println("輸出結果為:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}

控制台列印:

Caused by: java.lang.RuntimeException: 實例已經存在,請通過 getInstance()方法獲取!
    at cn.van.singleton.demo.mode.StaticInnerClassModeProtection.<init>(StaticInnerClassModeProtection.java:22)
    ... 35 more

四、總結

4.1 各種實現的對比

名稱 餓漢模式 懶漢模式 雙重檢查鎖模式 靜態內部類實現 枚舉類實現
可用性 可用 不推薦使用 推薦使用 推薦使用 推薦使用
特點 不能實現懶載入,可能造成空間浪費 不加鎖線程不安全;加鎖性能差 線程安全;延遲載入;效率較高 避免了線程不安全,延遲載入,效率高。 寫法簡單;線程安全;只裝載一次

4.2 示例代碼地址

Github 示例代碼


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • jQuery介紹 在說jQuery之前,先說一個概念吧,什麼是JavaScript框架庫,其實就是一個普通的js文件,裡面封裝了很多函數或者說封裝了很多相容的代碼;當然啦,jQuery就是眾多庫的一員,那麼我們為什麼要學習jQuery呢,那就講一下他的特點; 1. 很好的解決了不同瀏覽器之間的相容性 ...
  • 1.判斷輸入值的長度 1.1 根據輸入值的類型不同,限制輸入值長度不同 此時需要使用自定義的校驗規則。 如長度要求:中文輸入5位,非中文10位 1 <FormItem label="名稱" {...formItemLayout}> 2 {getFieldDecorator('name', { 3 r ...
  • Node.js是一個基於 Chrome V8 引擎的 JavaScript 運行環境;Node.js使用一個事件驅動、非阻塞式 I/O 的模型,使其輕量且高效;Node.js的軟體包生態系統npm是全球最大的開源庫生態系統。本文詳細介紹了Node.js的安裝、配置及測試教程,希望對您有所幫助。 ...
  • 前言 Koa 應用程式是一個包含一組中間件函數的對象,它是按照類似堆棧的方式組織和執行的。 當一個中間件調用 next() 則該函數暫停並將控制傳遞給定義的下一個中間件。當在下游沒有更多的中間件執行後,堆棧將展開並且每個中間件恢復執行其上游行為。 以上兩句話,是我在官方文檔中找到其對 Koa 中間件 ...
  • 下載:curl -L https://get.daocloud.io/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose 許可權:chm ...
  • 根據這個問題和答案 - Python multiprocessing.cpu_count()在4核Nvidia Jetson TK1上返回'1' - Python multiprocessing.cpu_count()在某些系統上的功能輸出反映了主動使用的CPU數量,而不是CPU的數量實際上可以通過 ...
  • 模塊導入 當文件夾中有這樣一個自定義的command模塊 在它的內部寫下下列代碼: 然後我們在command模塊中執行下列代碼: 我們如果在這段程式中反覆執行多次這一段代碼,這一個文件結果也只會被導入一次 調用command模塊中的方法fuc() 當我們寫這樣一個代碼的時候,電腦會先去找到這一個模 ...
  • 一、typedef的用法 1.用typedef來聲明新的類型名,來代替已有的類型名,也就是給類型起別名。比如 這種用法經常用來作為定義與平臺無關的類型,方便代碼的跨平臺移植。 例如,定義REAL類型為目標平臺精度最高的類型。 1>在支持long double的平臺上定義為: 註:long doubl ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...