從源碼中學習設計模式系列——單例模式序/反序列化以及反射攻擊的問題(二)

1、前言

這篇文章是學習單例模式的第二篇,以前的文章一會兒就給出來看起來很高大上的實現方法,可是這種模式仍是存在漏洞的,具體有什麼問題,你們能夠停頓一下子,思考一下。好了,不賣關子了,下面咱們來看看每種單例模式存在的問題以及解決辦法。設計模式

2、每種Singleton 模式的演進

  •  模式一
public class LazySingleton
    {
        private static LazySingleton lazySingleton = null;
        private LazySingleton()
        {

        }

        public static LazySingleton GetInstance()
        {
            if (lazySingleton == null)
            {
                lazySingleton = new LazySingleton();
            }

            return lazySingleton;
        }
    }

 

問題:該模式下在多線程下就會存在問題,由於你不知道線程執行的前後順序,不信看下面的調試,以下。緩存

咱們如今讓線程Two執行,它會進入到if裏面,由於線程one已經被凍結,調試結果:多線程

接着,咱們把凍結的線程one解凍,執行完成的結果以下:併發

發現,居然產生了兩個實例,這也就說明了上面實現單例模式在多線程下確實存在問題,爲了解決在多線程的問題,引出了下面的單例模式。函數

  • 模式二:DoubleCheck雙重檢查

問題:上面的代碼已經加上了lock,能夠解決多線程的問題,可是這樣仍是會出現問題,出現問題的地方在上面的兩處斷點處。多線程在多核CPU上執行時寄存器緩存和指令的從新排序【也就是new關鍵字步驟2和步驟3交換】雖然出現的機率很小,可是這種隱患必定要消除。若是出現指令重排的話,一個線程還沒來得及把分配對象的指針複製給變量lazySingleton,另一個線程就會進入到第一個斷點的if邏輯裏面。下面分別貼出寄存器緩存和指令從新排序的示意圖:學習

緩存數據示意圖:url

 

(注意:圖片來源自https://theburningmonk.com/2010/03/threading-understanding-the-volatile-modifier-in-csharp/)spa

現代計算機中的內存很複雜,有多級緩存,處理器寄存器和多個處理器共享主內存等。處理器可能會從主內存中讀取數據緩存到寄存器中,另外一個線程可能會使用緩存的數據,而且若是修改僅更新主內存,再次期間併發運行在另一個CPU上的線程,可能讀取的仍是以前的值。 在此期間,在另外一個CPU上併發運行的另外一個線程可能已經從主存儲器中讀取了相同的數據位並使用了過期的數據版本。.net

指令重排示意圖(下面的示意圖來自:geely老師的Java設計模式課程):線程

 對於單線程來講既是指令重排也不會影響,可是對於多線程就會有影響,以下圖所示:

 

爲了解決上面的問題有兩種作法:1)不容許2和3進行指令重排序。2)容許線程0能夠重排序可是不容許線程1重排序。

對於解決辦法1:可使用volatile關鍵字,它能夠禁止重排序以及緩存的問題。

對於解決辦法2:靜態內部類-基於類初始化的延遲加。

  • 模式三:解決辦法1示例代碼:

  • 模式四:解決辦法2示例代碼:
 public class StaticInnerClassSingleton
    {
        private static class InnerClass
        {
            internal static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
        }

        public static StaticInnerClassSingleton GetInstance()
        {
            return InnerClass.staticInnerClassSingleton;
        }
    }

 

static void GetInstancev5()
        {
            var hashCode = StaticInnerClassSingleton.GetInstance().GetHashCode();

            Console.WriteLine(hashCode);
        }

 

for (int i = 0; i < 10; i++)
            {
                Thread thread = new Thread(GetInstancev5);
                thread.Start();

                if (i%2==0)
                {
                    Thread.Sleep(1000);
                }
            }

 

驗證結果:

 

模式五:餓漢模式

public class CurrentSingleton
    {
        private static CurrentSingleton uniqueInstance = new CurrentSingleton();
        private CurrentSingleton() {
           
        }
        public static CurrentSingleton Instance
        {
            get { return uniqueInstance; }
        }
    }

 

 

 聊到這裏,關於單例模式的幾種模式已經差很少了,該聊的已經聊完了,大多小夥伴們可能就瞭解到這裏就結束了,先舒口氣,再繼續往下看,你會有意向不到的收穫。

 

3、單例模式下的問題解決辦法

  •  問題一:反射攻擊單例模式三

單例模式三(懶漢模式)代碼:

 public class LazyDoubleCheckSingleton
    {
        private volatile static LazyDoubleCheckSingleton lazySingleton = null;
        private static readonly object _threadSafetyLock = new object();

        private LazyDoubleCheckSingleton(){}

        public static LazyDoubleCheckSingleton GetInstance()
        {
            if (lazySingleton == null)
            {
                lock(_threadSafetyLock)
                {
                    if (lazySingleton == null)
                    {
                        //注意:new關鍵字作了下面三步的工做:
                        //一、分配內存給這個對象
                        //二、初始化對象
                        //三、設置lazySingleton指向剛分配的內存地址
                        lazySingleton = new LazyDoubleCheckSingleton();
                    }
                }
            }

            return lazySingleton;
        }
    }

 

看到沒,咱們經過反射也能夠建立類的實例,那怕你的構造函數是private的,我經過反射均可以來建立對象的實例。同理你能夠嘗試使用該方法來攻擊模式五(餓漢模式)。

那咱們該如何防護?對於餓漢模式、基於靜態類模式的單例,咱們能夠經過下面的方法來防護:

在對應的private構造函數中添加一下代碼:

 

對於懶漢模式的單例這種方法還適用嗎?不必定,請看下面的代碼:

基於模式三【見上】的代碼修改:

驗證結果:

發現該方式處理不起做用。對於這個問題咱們該怎麼解決?嘗試的方法以下:

 public class LazyDoubleCheckSingleton
    {
        private volatile static LazyDoubleCheckSingleton lazySingleton = null;
        private static readonly object _threadSafetyLock = new object();
        private static bool flag = true;

        private LazyDoubleCheckSingleton(){
            if (flag)
            {
                flag = false;
            }
            else
            {
                throw new Exception("單例構造器進制反射調用");
            }
        }

        public static LazyDoubleCheckSingleton GetInstance()
        {
            if (lazySingleton == null)
            {
                lock(_threadSafetyLock)
                {
                    if (lazySingleton == null)
                    {
                        //注意:new關鍵字作了下面三步的工做:
                        //一、分配內存給這個對象
                        //二、初始化對象
                        //三、設置lazySingleton指向剛分配的內存地址
                        lazySingleton = new LazyDoubleCheckSingleton();
                    }
                }
            }

            return lazySingleton;
        }
    }

 

Type type = typeof(LazyDoubleCheckSingleton);
            object sobj = Activator.CreateInstance(type, true);

            
            Console.WriteLine(LazyDoubleCheckSingleton.GetInstance().GetHashCode());
            Console.WriteLine(sobj.GetHashCode());

 

驗證結果:

這種方法看似解決了懶漢模式的問題,可是!它真的能解決這個問題嗎?你們能夠想一下,爲何解決不了?我也就不賣關子了,緣由就是反射,反射的威力太強了,上面演示的,即便你的構造函數是private我也能建立對象,區區一個字段,反射修改你的值不是很輕鬆嗎。

反射攻擊演示:

因此懶漢模式的單例,是防護不了反射攻擊的,至於Java中有一個叫枚舉模式的單例,能夠解決這個問題,至於C#目前我還沒想出好的解決辦法,若是你們有好的解決辦法能夠貢獻到評論區。好了問題一講到這裏已經差很少了,下面咱們來介紹問題二。

  • 問題:序列化破壞單例模式

背景:在某些場景下咱們須要把類序列化到文件當中,正好這個類是單例的,正常的狀況應該是:序列化到文件中,再從文件反序列化,應該是同一個類,但通常的處理方法真的能獲得同一個類嗎?

實例代碼:

[Serializable]
public
class StaticInnerClassSingleton { private static class InnerClass { internal static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton GetInstance() { return InnerClass.staticInnerClassSingleton; } }
//序列化到文件:

            var obj = StaticInnerClassSingleton.GetInstance();
            var formatter = new BinaryFormatter();
            var stream = new FileStream("D:\\Example.txt", FileMode.Create, FileAccess.Write);

            formatter.Serialize(stream, obj);
            stream.Close();


            //從文件讀取出來反序列化

            stream = new FileStream("D:\\Example.txt", FileMode.Open, FileAccess.Read);

            var obj2 = (StaticInnerClassSingleton)formatter.Deserialize(stream);

            Console.WriteLine(obj.GetHashCode());
            Console.WriteLine(obj2.GetHashCode());

 

驗證結果:

看到沒,居然是兩個不一樣的實例,若是你們遇到這樣的場景可使用下面的方法來保障反序列化出來的是同一個對象,咱們只須要修改單例模式的類。代碼以下:

[Serializable] public class StaticInnerClassSingleton: ISerializable { private StaticInnerClassSingleton()
        {

        }

        private static class InnerClass
        {
            internal  static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
        }

        public static StaticInnerClassSingleton GetInstance()
        {
            return InnerClass.staticInnerClassSingleton;
        }

       
        public void GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof(SingletonHelper)); } [Serializable] private class SingletonHelper : IObjectReference { public object GetRealObject(StreamingContext context) { return InnerClass.staticInnerClassSingleton; } }
    }

 

 

 若是想知道爲何要這樣寫我就不在解釋了,你們能夠參考這篇文章:http://geekswithblogs.net/maziar/archive/2012/07/19/serializing-singleton-objects-c.aspx   好了講到這裏基本上單例這種設計模式,你已經掌握的很是好了,但願對你有幫助,謝謝,若是以爲不錯的話,能夠推薦一下。以前一直想寫這個系列的博客,但願把本身平時學的和工做中的經驗分享出來,共同進步,這個系列的標題是「從源碼中學習設計模式

 這裏的源碼主要就是ASP.Net Core2.1的源碼,如今.Net Core 3.0已是預覽版,尚未正式版,也但願.Net Core 愈來愈好。也但願個人文章能對你有幫助。

4、總結

 

 單例這種設計模式,具體使用哪一種要看你的使用場景,並非那種模式必定就好,這是須要權衡的,但願看完本篇文章,你在使用該模式能駕輕就熟。另外你們不要和依賴注入中的單例混淆,以前再介紹依賴注入最佳實踐的文章中有園友就混淆了。

 

 

 

參考資料:

geely老師的《Java設計模式精講》

做者:郭崢

出處:http://www.cnblogs.com/runningsmallguo/

本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文連接。

相關文章
相關標籤/搜索