單例模式使用餓漢式和懶漢式創建一定安全?很多人不知

来源:https://www.cnblogs.com/mhg215/archive/2022/08/03/16548218.html
-Advertisement-
Play Games

概述 單例模式大概是23種設計模式裡面用的最多,也用的最普遍的了,也是很多很多人一問設計模式都有哪些必答的第一種了;我們先複習一下餓漢式和懶漢式的單例模式,再談其創建方式會帶來什麼問題,並一一解決!還是老規矩,先上代碼,不上代碼,紙上談兵咱把握不住。 餓漢式代碼 public class Singl ...


概述

單例模式大概是23種設計模式裡面用的最多,也用的最普遍的了,也是很多很多人一問設計模式都有哪些必答的第一種了;我們先複習一下餓漢式和懶漢式的單例模式,再談其創建方式會帶來什麼問題,並一一解決!還是老規矩,先上代碼,不上代碼,紙上談兵咱把握不住。

餓漢式代碼

    public class SingleHungry
    {
        private readonly static SingleHungry _singleHungry = new SingleHungry();
        private SingleHungry()
        {
        }
        public static SingleHungry GetSingleHungry()
        {
            return _singleHungry;
        }
    }

代碼很簡單,意思也很明確,接著我們寫點代碼測試驗證一下;

第一種測試: 構造函數私有的,new的時候報錯,因為我們的構造函數是私有的。

 SingleHungry  _singleHungry=new SingleHungry();
第二種測試: 比對創建多個對象,然後多個對象的Hashvalue
public class SingleHungryTest
    {
        public static void FactTestHashCodeIsSame()
        {
            Console.WriteLine("單例模式.餓漢式測試!");
            var single1 = SingleHungry.GetSingleHungry();
            var single2 = SingleHungry.GetSingleHungry();
            var single3 = SingleHungry.GetSingleHungry();
            Console.WriteLine(single1.GetHashCode());
            Console.WriteLine(single2.GetHashCode());
            Console.WriteLine(single3.GetHashCode());
        }
    }
測試下來,三個對象的hash值是一樣的。如下圖:

餓漢式結論總結

餓漢式的單例模式不推薦使用,因為還沒調用,對象就已經創建,造成資源的浪費;

懶漢式代碼

    public class SingleLayMan
    {
        //1、私有化構造函數
        private SingleLayMan()
        {

        }
        //2、聲明靜態欄位  存儲我們唯一的對象實例
        private static SingleLayMan _singleLayMan;
        //通過方法 創建實例並返回
        public static SingleLayMan GetSingleLayMan1()
        {
            //這種方式不可用  會創建多個對象,謹記
            return _singleLayMan = new SingleLayMan();
        }
        /// <summary>
        ///懶漢式單例模式只有在調用方法時才會去創建,不會造成資源的浪費
        /// </summary>
        /// <returns></returns>
        public static SingleLayMan GetSingleLayMan2()
        {
            if (_singleLayMan == null)
            {
                Console.WriteLine("我被創建了一次!");
                _singleLayMan = new SingleLayMan();
            }
            return _singleLayMan;
        }
    }

測試代碼

 public class SingleLayManTest
    {
        /// <summary>
        /// 會創建多個對象.hash值不一樣
        /// </summary>
        public static void FactTest()
        {
            Console.WriteLine("單例模式.懶漢式測試!");
            var singleLayMan1 = SingleLayMan.GetSingleLayMan1();
            var singleLayMan2 = SingleLayMan.GetSingleLayMan1();
            Console.WriteLine(singleLayMan1.GetHashCode());
            Console.WriteLine(singleLayMan2.GetHashCode());
        }
        /// <summary>
        /// 單例模式.懶漢式測試:懶漢式單例模式只有在調用方法時才會去創建,不會造成資源的浪費,但會有線程安全問題
        /// </summary>
        public static void FactTest1()
        {
            Console.WriteLine("單例模式.懶漢式測試!");
            var singleLayMan1 = SingleLayMan.GetSingleLayMan2();
            var singleLayMan2 = SingleLayMan.GetSingleLayMan2();
            Console.WriteLine(singleLayMan1.GetHashCode());
            Console.WriteLine(singleLayMan2.GetHashCode());
        }
        /// <summary>
        /// 單例模式.懶漢式多線程環境測試!
        /// </summary>
        public static void FactTest2()
        {
            Console.WriteLine("單例模式.懶漢式多線程環境測試!");
            for (int i = 0; i < 10; i++)
            {
                new Thread(() =>
                {
                    SingleLayMan.GetSingleLayMan2();
                }).Start();
            }

            //Parallel.For(0, 10, d => {
            //    SingleLayMan.GetSingleLayMan2();
            //});
        }
    }

懶漢式結論總結

懶漢式的代碼如上已經概述,上面GetSingleLayMan1()會創建多個對象,這個沒什麼好說的,肯定不推薦使用;GetSingleLayMan2()是大多數人經常使用的,可解決剛纔因為餓漢式創建帶來的缺點,但也帶來了多線程的問題,如果不考慮多線程,那是夠用了。



話說回來,既然剛纔餓漢式和懶漢式各有其優缺點,那我們該如何抉擇呢?到底選擇哪一種?

其它方式創建單例—餓漢式+靜態內部類

    public class SingleHungry2
    {
        public static SingleHungry2 GetSingleHungry()
        {
            return InnerClass._singleHungry;
        }       
        public static class InnerClass
        {
            public readonly static SingleHungry2 _singleHungry = new SingleHungry2();
        }
    }

這個代碼,用了餓漢式結合靜態內部類來創建單例,線程也安全,不失為創建單例的一種辦法。

其它方式創建單例—懶漢式+反射

 首先我們解決一下剛纔懶漢式創建單例的線程安全問題,上代碼:

 /// <summary>
    /// 通過反射破壞創建對象
    /// </summary>
    public class SingleLayMan1
    { 
        //私有化構造函數
        private SingleLayMan1()
        {
        }
        //2、聲明靜態欄位  存儲我們唯一的對象實例
        private static SingleLayMan1? _singleLayMan;
        private static object _oj = new object();

/// <summary> /// //解決多線程安全問題,雙重鎖定,減少系統消耗,節約資源 /// </summary> public static SingleLayMan1 GetSingleLayMan() { if (_singleLayMan == null) { lock (_oj) { if (_singleLayMan == null) { _singleLayMan = new SingleLayMan1(); Console.WriteLine("我被創建了一次!"); } } } return _singleLayMan; } }

具體描述,在代碼裡面已經說得足夠清楚,一看肯定明白,我們還是寫點測試代碼,驗證一下,上代碼:

public class SingleLayManTest1
    {
        public static void FactTestReflection()
        {
            var singleLayMan1= SingleLayMan1.GetSingleLayMan();

            var type = Type.GetType("_01單例模式.反射破壞單例模式.SingleLayMan1");
            //獲取私有的構造函數
            var ctors = type?.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
            //執行構造函數
            SingleLayMan1 singleLayMan = (SingleLayMan1)ctors[0].Invoke(null);
            Console.WriteLine(singleLayMan1.GetHashCode());
            Console.WriteLine(singleLayMan.GetHashCode());
        }
    }

上面的代碼分別通過SingleLayMan1.GetSingleLayMan2()和反射創建對象,輸出二者對象hash值比較,結果肯定是不一樣的,重點是我們可以通過反射創建對象。

通過上面的代碼,不知道大家有沒有意識到我們雖通過加鎖解決了線程安全問題,但仍會出現問題;正常創建對象的順序是:

1、new 在記憶體中開闢空間
2、 執行構造函數 創建對象
3、 把空間指向我們的對像

但如果因為我們的程式使用多線程,則會發生"指令重排",本應執行順序為1、2、3,實際執行順序為1、3、2,但這種情況很少,不過我們寫程式嘛,肯定追求嚴謹一點準沒錯。

如果需要解決該問題需要給定義的私有局部變數加關鍵字 加上volatile (意思不穩定的 ,可變的) ,加該關鍵字可以避免指令重排。具體代碼主要是這句如下:

 private volatile static SingleLayMan? _singleLayMan;

 

到這裡,大家認為還有沒有問題?答案是肯定的,不然我就不會寫這篇文章了,通過反射既然可以創建對象,那麼我們寫的創建實例代碼還有什麼意義,有沒有什麼辦法避免反射創建對象呢?

如果認真看了之前的反射創建對象代碼,肯定發現反射是通過構造函數來創建對象的,那麼我們相應的就在構造函數處理一下。來,我們繼續上代碼:

 /// <summary>
    /// 解決反射創建對象的問題
    /// </summary>
    public class SingleLayMan3
    {
        //2、聲明靜態欄位  存儲我們唯一的對象實例
        private volatile static SingleLayMan3? _singleLayMan;
        private static object _oj = new object();
        //私有化構造函數
        private SingleLayMan3()
        {
            lock (_oj)
            {
                if (_singleLayMan != null)
                {
                    throw new Exception("不要通過反射來創建對像!");
                }
            }
        }

        /// <summary>
        /// //解決多線程安全問題,雙重鎖定,減少系統消耗,節約資源
        /// </summary>
        public static SingleLayMan3 GetSingleLayMan()
        {
            if (_singleLayMan == null)
            {
                lock (_oj)
                {
                    if (_singleLayMan == null)
                    {
                        _singleLayMan = new SingleLayMan3();
                        Console.WriteLine("我被創建了一次!");
                    }
                }
            }           
            return _singleLayMan;
        }
       
    }

下麵繼續上測試代碼,驗證一下:

public class SingleLayManTest3
    {
        /// <summary>
        /// 第一次通過調用 SingleLayMan3.GetSingleLayMan()創建對象導致_singleLayMan不為空,之後再去通過反射創建對象時,構造函數裡面判斷創建對象導致_singleLayMan變數,報異常
        /// </summary>
        public static void FactTestReflection()
        {
            var singleLayMan1= SingleLayMan3.GetSingleLayMan();

            var type = Type.GetType("_01單例模式.反射破壞單例模式.SingleLayMan3");
            //獲取私有的構造函數
            var ctors = type?.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
            //執行構造函數
            SingleLayMan3 singleLayMan = (SingleLayMan3)ctors[0].Invoke(null);
            Console.WriteLine(singleLayMan1.GetHashCode());
            Console.WriteLine(singleLayMan.GetHashCode());
        }
    }

結論其實測試方法已經說明:第一次通過調用 SingleLayMan3.GetSingleLayMan()創建對象導致_singleLayMan不為空,之後再去通過反射創建對象時,構造函數裡面判斷創建對象導致_singleLayMan變數,報異常。

其實到這裡,有人肯定發現了問題,第一次通過去執行自己寫的創建單例方法來創建對象,後面再執行反射時才會報異常,那有沒有什麼辦法,只要有人第一次反射創建對象時就報異常呢?

定義局部變數解決反射創建對象問題

 public class SingleLayMan4
    {
        //2、聲明靜態欄位  存儲我們唯一的對象實例
        private volatile static SingleLayMan4? _singleLayMan;
        private static object _oj = new object();
        private static bool _isOk = false;
        //私有化構造函數
        private SingleLayMan4()
        {
            lock (_oj)
            {
                if (_isOk == false)
                {
                    _isOk = true;
                }
                else
                {
                    throw new Exception("不要通過反射來創建對像!只有第一次通過反射創建對象會成功!請做第一個吃葡萄的人!");
                }
            }
        }

        /// <summary>
        /// //解決多線程安全問題,雙重鎖定,減少系統消耗,節約資源
        /// </summary>
        public static SingleLayMan4 GetSingleLayMan()
        {
            if (_singleLayMan == null)
            {
                lock (_oj)
                {
                    if (_singleLayMan == null)
                    {
                        _singleLayMan = new SingleLayMan4();
                        Console.WriteLine("我被創建了一次!");
                    }
                }
            }           
            return _singleLayMan;
        }
       
    }

測試代碼,驗證一下:

public static void FactTestReflection()
        {
            //第一次創建對象會成功
            var singleLayMan1 = GetReflectionSingleLayMan4Instance();

            //第二次創建對象會失敗,報異常
           var singleLayMan2 = GetReflectionSingleLayMan4Instance();

            Console.WriteLine(singleLayMan1.GetHashCode());
        }
        private static SingleLayMan4 GetReflectionSingleLayMan4Instance()
        {
            var type = Type.GetType("_01單例模式.反射破壞單例模式.SingleLayMan4");
            //獲取私有的構造函數
            var ctors = type?.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
            //執行構造函數
            SingleLayMan4 singleLayMan = (SingleLayMan4)ctors[0].Invoke(null);
            return singleLayMan;
        }

第一次創建對象會成功,因為執行構造函數時沒有執行GetSingleLayMan(),跨過了new,導致_isOk賦值true,第二次反射創建執行構造函數時判斷變數_isOk為true,走入異常邏輯。

但這樣做真的就安全了嗎?既然可以通過反射執行構造函數來創建對象,那也可以通過反射改變局部變數_isOk 的值,上代碼:

        /// <summary>
        /// 通過反射也可以改變局部變數_isOk的值,繼續創建對象
        /// </summary>
        public static void FactTestReflection2()
        {
            Type type = Type.GetType("_01單例模式.反射破壞單例模式.SingleLayMan4");
            //獲取私有的構造函數
            var ctors = type?.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
            //執行構造函數
            SingleLayMan4 singleLayMan1 = (SingleLayMan4)ctors[0].Invoke(null);
            FieldInfo fieldInfo =  type.GetField("_isOk", BindingFlags.NonPublic | BindingFlags.Static);
            fieldInfo.SetValue("_isOk", false);
            SingleLayMan4 singleLayMan2 = (SingleLayMan4)ctors[0].Invoke(null);

            Console.WriteLine(singleLayMan1.GetHashCode());
            Console.WriteLine(singleLayMan2.GetHashCode());
        }

最後

大家或許發現了,只要有反射存在,哪怕你的邏輯寫的再嚴謹,它仍然可以反射創建對象,只因為它是反射!所以,單例模式的安全性也是相對而言的,具體選擇用哪個,取決項目的業務場景了。如有發現問題,歡迎不吝賜教!

源碼地址:https://gitee.com/mhg/design-mode-demo.git




    

作者:課間一起牛

出處:https://www.cnblogs.com/mhg215/

聲援博主:如果您覺得文章對您有幫助,請點擊文章末尾的【關註我】吧!

別忘記點擊文章右下角的【推薦】支持一波。~~~///(^v^)\\\~~~ .

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

如果您有其他問題,也歡迎關註我下方的公眾號,可以聯繫我一起交流切磋!

碼雲:碼雲      github:github


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

-Advertisement-
Play Games
更多相關文章
  • 1.認證流程分析 Spring Security中預設的一套登錄流程是非常完善並且嚴謹的。但是項目需求非常多樣化, 很多時候,我們可能還需要對Spring Secinity登錄流程進行定製,定製的前提是開發者先深刻理解Spring Security登錄流程,然後在此基礎之上,完成對登錄流程的定製。本 ...
  • 如果程式中有大量的計算任務,並且這些任務能分割成幾個互相獨立的任務塊,那就應該使用並行編程。 並行編程用於分解計算密集型的任務片段,並將它們分配給多個線程。這些並行處理方法只適用於計算密集型的任務。 一 數據的並行處理 如果有一批數據,需要對每個數據進行相同的操作,其操作是計算密集型的,需要耗費一定 ...
  • 多態 靜態多態性,重載 同一個方法中有多個相同名稱的方法,但參數不一樣。 在編譯階段(程式未運行的時候),函數之間就產生了一對一的關係。 減少函數的命名,多個相同的函數可以使用相同的命名。 Mathf f = new Mathf(); f.Add(10001); class Mathf { publ ...
  • WPF的ObservableCollection在增刪改的時候,通過繼承INotifyCollectionChanged使用CollectionChanged通過依賴屬性發生了變化。(本篇的例子從:https://blog.lindexi.com/post/win10-uwp-%E9%80%9A%E ...
  • 一、C#數據類型 值類型:直接訪問數據的值。有基本數據類型(byte / short / int / long / float / double / char / bool)、struct、enum; 引用類型:訪問數據的存儲地址。有class、interface、數組、委托、stting; 值類型 ...
  • 一、前言 在windows平臺軟體開發過程中,註冊表的操作是經常會遇到的一個場景。今天記錄一下在操作註冊表時遇到的一些坑; 二、正文 1、操作註冊表,於是直接從網上找了一段代碼來用 /// <summary> /// 讀取註冊表 /// </summary> /// <param name="nam ...
  • 前言 接著上周寫的截圖控制項繼續更新添加 畫筆。 1.WPF實現截屏「仿微信」 2.WPF 實現截屏控制項之移動(二)「仿微信」 3.WPF 截圖控制項之伸縮(三) 「仿微信」 4.WPF 截圖控制項之繪製方框與橢圓(四) 「仿微信」 5.WPF 截圖控制項之繪製箭頭(五)「仿微信」 6.WPF 截圖控制項之繪 ...
  • 此案例包含了簡單的碰撞檢測,圓形碰撞檢測方法,也可以說是五環彈球的升級版,具體可以根據例子參考。 粒子花園 這名字是案例的名字,效果更加具有科技感,很是不錯,搞搞做成背景特效也是不錯的選擇。 Wpf 和 SkiaSharp 新建一個 WPF 項目,然後,Nuget 包即可 要添加 Nuget 包 I ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...