著名的雙檢鎖技術


最近公司的項目中發現一個編譯優化導致的bug。同事敘述為CPU開啟out-of-order execution優化時,是有bug的”。針對這個問題,比較好的優化方法如下:

        private static JobManager self;
        private static object asyncObj = new object();

        public static JobManager Instance
        {
            get
            {
                if (self == null)
                {
                    lock (asyncObj)
                    {
                        if (self == null)
                        {
                            // 正確的實現方法應該為: var temp = new JobManager(); Interlocked.Exchange(ref self, temp);
                            self = new JobManager();
                        }
                    }
                }
                return self;
            }
        }

這里需要解釋一下:

self = new JobManager()

這句你的本意是為 JobManager 分配內存,調用構造器初始化字段,再將引用賦給 self ,即發布出來讓其他線程可見。但是,那只是你一廂情願的想法,編譯器可能這樣做:為JobManager 分配內存,將引用發布到(賦給)self,再調用構造器。然而,如果在將引用發布給 self 之后,調用構造器之前,另一個線程發現 self 不為 null,便開始使用JobManager對象,這時會發生什么?這個時候對象的構造器還沒有執行結束!這是一個很難追蹤的bug。

 

從雙檢鎖技術的角度來看,使用 Interlocked.Exchange確實是最好的解決方案。但有兩個問題,它該如何解決?
1.速度是否夠快?
2.如果一個線程池線程在Monitor的線程同步構造上阻塞,線程池會創建另一個線程來保持CPU的“飽和”,而創建一個新線程的代價是很昂貴的,我們該如何避免這樣的情況?
 
試着跳出“lock+2次if”的框子,我們可以使用 Interlocked.CompareExchange來解決上面的問題。下面是一個示例:
    internal sealed class MySingleton
    {
        private static MySingleton s_value = null;
        public static MySingleton GetMySingleton()
        {
            if (s_value != null) return s_value;
 
            MySingleton temp = new MySingleton();
            Interlocked.CompareExchange(ref s_value, temp, null);
            return s_value;
        }
    }

雖然多個線程同時調用GetMySingleton,會創建2個或者更多的MySingleton對象,但沒有被s_value引用的臨時對象會在以后被垃圾回收。大多數應用程序很少會發生同時調用GetMySingleton的情況,所以不太可能出現創建多個MySingleton對象的情況。上述代碼帶來優勢是很明顯的,首先,它的速度是非常快,其次,它永不阻塞線程。這就解決了前面在雙檢鎖技術中提出的問題。

 
另外,在.net 4.0中提供了2個類型封裝上述兩種模式(雙檢鎖技術、使用 Interlocked.CompareExchange技術):
泛型 System.Lazy類和 System.Threading.LazyInitializer類。下面是2個示例:
        public static void Main()
        {
            Lazy<string> s = new Lazy<string>(() => DateTime.Now.ToLongTimeString(), LazyThreadSafetyMode.PublicationOnly);

            Console.WriteLine(s.IsValueCreated);
            Console.WriteLine(s.Value);
            Console.WriteLine(s.IsValueCreated);
            Thread.Sleep(5000);
            Console.WriteLine(s.Value);
            Console.WriteLine(DateTime.Now.ToLongTimeString());
        }
 
輸出結果:

 

 

        public static void Main()
        {
            string name = null;

            LazyInitializer.EnsureInitialized(ref name, () => "Benjamin");
            Console.WriteLine(name);
 
            LazyInitializer.EnsureInitialized(ref name, () => "Yao");
            Console.WriteLine(name);
        }

 

輸出結果:

 

其中枚舉 LazyThreadSafetyMode解釋如下:
    public enum LazyThreadSafetyMode
    {
        None = 0,     //完全沒有線程安全劫持(適合GUI應用程序)
        PublicationOnly = 1,      //使用Interlocked.CompareExchange技術
        ExecutionAndPublication = 2,     //使用雙檢鎖技術
    }

 


免責聲明!

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



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