2011-09-13 163 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。 –