2011-09-13 198 views
5

如果我想確保只創建一次實例,我是否需要在此處添加鎖定塊?我需要鎖嗎?

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

由於IF中只有1條指令,我不是100%確定的。在下面的情況下,我確定我需要它,但是我想仔細檢查是否同樣適用於上面的代碼。

 if (instance==null) 
     { 
      int i = 5; 
      int y = 78; 
      instance = new Class(i, y); 
     } 

編輯

是的,我認爲多線程

+2

可能值得[實施辛格爾頓Pattern in C#](http://csharpindepth.com/Articles/General/Singleton.aspx) –

回答

7

是的,你需要在你的兩個例子中鎖定。讓我們來數的行,使說明容易:

1 if (instance == null) 
2 { 
3  instance = new Class(); 
4 } 

現在,假設你有兩個線程,A和B兩個線程都執行此代碼。首先,在第1行測試instance,並且由於它爲空,它將採用真正的路徑 - 在第3行。然後,在第3行執行前和B執行相同的操作(第1行爲true,最後在3行)。現在這兩個線程都位於您的if聲明正文中,並且您將得到instance的兩個分配。

9

如果您正在多線程,那麼答案是肯定的。

小記:

顯然使用情況下,你不能把一個鎖,因爲它是空:-)

的鎖定模式是正常(這就是所謂的Double-checked locking):

if (instance == null) 
{ 
    lock (something) 
    { 
     if (instance == null) 
     { 
      instance = new Class(); 
     } 
    } 
} 

如果你想(如果創建類不貴),你可以做:

if (instance == null) 
{ 
    Interlocked.CompareExchange(ref instance, new Class(), null); 
    // From here you are sure the instance field containts a "Class" 
} 

這段代碼的唯一問題是兩個線程可能會創建一個新的Class(),但只有一個能夠使用新Class()的引用來設置實例,所以另一個線程會創建一個無用的Class對象那將是GC。如果創建Class對象是便宜的(例如創建一個List<T>)就沒關係。如果創建Class對象的代價很​​高(可能是因爲構造函數調用一個數據庫並做了一個大的查詢),那麼這個方法是一個禁忌。小記:格林奇已經到了,它已經宣佈所有這些多線程都不足夠「防萬無一失」。他是對的。他錯了。這就像是Schrödinger的貓! :-)上次我寫了一個多線程程序時,我只是讀了所有關於互聯網的文獻。而且我仍然犯了錯誤(但是,好吧,我正在嘗試編寫無鎖的MT代碼......這真是太重了)。因此,如果您使用的是.NET 4.0,那麼使用Lazy<T>,如果不行的話...拿單聲道源文件找到Lazy定義的位置並複製它:-)(許可證非常寬容,但是讀取它!) 但是如果什麼你需要的是一個單身人士,你可以使用static Lazy<T>或者如果你沒有.NET 4.0,你可以使用http://www.yoda.arachsys.com/csharp/singleton.html(第五個或第四個)的樣本。作者建議第四個。請注意,在那裏對它的懶惰有一些有趣的警告,但它們寫在http://www.yoda.arachsys.com/csharp/beforefieldinit.html)。請注意,你必須「寫作」。千萬不要想到它會偏離它所寫的內容。別。正如你可以看到通過評論閱讀,線程是一個硬性的論據。你可能是對的,同時你仍然可能是錯的。它就像揮發性物質(揮發性物質?是雙關語嗎?)化合物......非常非常有趣,非常非常危險。

另一個小提示:在.NET 1.1下它確實很粘。除非你完全知道你在做什麼,否則你不應該在1.1中的線程之間共享變量。在2.0版中,他們改變了內存模型(編譯器如何優化對內存的訪問),並且他們創建了一個「更安全」的內存模型。 .NET 1.1和更早版本的Java(包括1.4版本)都是坑陷阱。


要回答你的問題,一個簡單的技巧:當你在想「MT可以打破我的代碼嗎?」這樣做:想象一下Windows(操作系統)是一個懶惰的食人魔。有時候他讓一個線程停下半個小時,同時讓其他線程運行(技術上他可以做到這一點),而不是30分鐘(但沒有關於多長時間的真正規則),但是在毫秒內它可以並且如果處理器有一些工作被重載,並且有很多線程固定在特定的處理器上(固定意味着他們告訴操作系統他們只想在某些特定的處理器上運行,所以如果1000個線程固定在處理器1上,而處理器2只能執行1線程沒有固定,很明顯,處理器2上的線程將會更快!))。所以想象一下,兩個線程同時輸入你的代碼段,同時執行第一行(它們在兩個不同的處理器上,它們可以並行地執行),但是兩個線程中的一個停止並且具有等待30分鐘。同時會發生什麼?並且請注意,經常在代碼中間停止代碼! a = a + 1是兩條指令!它是var temp = a + 1; a = temp;如果你將這個技巧應用到示例代碼中,很容易看出:兩個線程都執行if (instance==null)並傳遞,然後一個線程停止30分鐘,另一個線程初始化該對象,第一個線程恢復並初始化目的。兩個對象初始化。沒有良好的:-)

我會用一個簡單的例子解釋了.NET 1.1的問題:

class MyClass 
{ 
    public bool Initialized; 

    public MyClass() 
    { 
     Initialized = true; 
    } 
} 

MyClass instance = null; 

public MyClass GetInstance() 
{ 
    if (instance == null) 
    { 
     lock (something) 
     { 
      if (instance == null) 
      { 
       instance = new Class(); 
      } 
     } 
    } 

    return instance; 
} 

現在...這是代碼之前。該問題發生在instance = new Class()行。讓我們分開它的各個部分:

  1. Class對象的空間由.NET分配。這個空間的引用被保存在某個地方。
  2. Class的構造函數被調用。 Initialized = true(該字段稱爲Initialized!)。
  3. 實例變量被設置爲我們之前保存的引用。

在.NET 2.0的較新「強大」內存模型中,這些操作將按此順序進行。但讓我們看看.NET 1.1會發生什麼:寫入可以重新排序!

  1. Class對象的空間由.NET分配。這個空間的引用被保存在某個地方。
  2. 實例變量被設置爲我們之前保存的引用。
  3. Class的構造函數被調用。 Initialized = true(該字段稱爲Initialized!)。

現在讓我們想象一下線程執行,這是由OS 30分鐘暫停2點後點3之前另一個線程可以訪問實例變量(請注意,在代碼中的第一個,如果沒有被保護通過一個鎖,所以它可以訪問它而不用等待第一個線程結束它的工作)並使用它。但類沒有真正初始化:他的構造函數沒有運行! Boooom!如果您在實例聲明(如此volatile MyClass instance = null;)上使用了volatile關鍵字,則不會發生這種情況,因爲編譯器無法重新排序寫入超出易失性字段上的寫入。所以他不能在點3之後重新排序點2,因爲在點3中它正在寫入一個易失性字段。但正如我寫的,這是.NET 1.1的一個問題。

現在。如果您想了解線程,請閱讀以下內容:http://www.albahari.com/threading/如果您想知道「幕後」會發生什麼,您可以閱讀http://msdn.microsoft.com/en-us/magazine/cc163715.aspx,但它很沉重。

+4

即使答案是YES。 – Thomas

+0

雙重檢查鎖定被打破 –

+0

更好地使用'懶惰' - 它是安全的,更容易得到正確。 –

2

如果您使用多個線程,是的。多個線程可以在任何實際將實例設置爲新類之前輸入if語句。

8

如果這是多線程的,那麼是的,你需要一個鎖或某種形式的同步。

但是,如果這是爲了允許延遲實例化,我建議使用Lazy<T>。它爲您處理線程安全性,並且不需要檢查。

+0

對不起,我正在使用.NET 3.5 – StackOverflower

+0

@Timmy如果我沒有記錯,惰性是在Reactive Framework中引入的,它們在C#4.0中引入的Concurrent集合的很大一部分。 – xanatos

+0

@Timmy:它是該框架的一部分。這是一個很好的方式來獲得它和.NET 3.5的TPL。 –