据我所知,以下代码是线程安全的,但需要注意的是我不是在寻找单例模式(我不关心多个线程是否因为MyObject是不可变的而获得MyObject的不同实例。创建多个实例的并发线程之间的潜在争用,后续线程将获得相同的intance)。
private static MyObject myObject;
...
public static MyObject GetMyObject()
{
if (myObject == null)
{
myObject = new MyObject(...);
}
return myObject;
}
以下显然不是线程安全的,因为多个线程可能会在完全初始化之前尝试访问myObject字段:
private static MyObject myObject;
...
public static MyObject GetMyObject()
{
if (myObject == null)
{
myObject = new MyObject();
myObject.Initialize(...);
}
return myObject;
}
以下是线程安全的,还是myObject字段需要volatile关键字? 看起来是安全的,因为在对象完全初始化之前不会分配myObject字段。但我担心JITter可能能够内联LoadMyObject方法并基本上重新实现它作为上面的代码,这不是线程安全的。
private static MyObject myObject;
...
private static MyObject LoadMyObject()
{
MyObject myObject = new MyObject();
myObject.Initialize(...);
return myObject;
}
public static MyObject GetMyObject()
{
if (myObject == null)
{
myObject = LoadMyObject();
}
return myObject;
}
上面的背景是我想重构一些多线程代码,并通过调用工厂方法替换对构造函数的调用。我想知道这样做是否会破坏线程安全。
修改
重申一下,我不关心是否创建了多个MyObject实例,只是没有办法访问未完全初始化的实例。作为一个具体的例子,想一下加载了配置文件内容的只读XmlDocument。我想要一个对内存中可用的XmlDocument的静态引用,以避免在每次访问时从磁盘加载的开销。但我不在乎两个线程是否碰巧并发运行并且都从磁盘加载文件。
EDIT2
如果我理解正确,C#语言规范中的以下语句似乎暗示JITter不会重新排序我的代码,因此我的上一个样本应该是线程安全的。我理解正确吗?
3.10执行顺序 ... 数据依赖保留在一个 执行的线程。那就是 每个变量的值计算为 如果线程中的所有语句都是 以原始程序顺序执行
答案 0 :(得分:2)
在某些情况下,易失性可能是一种性能选择,以避免使用锁的额外命中,但它不能解决竞争条件的线索安全问题,这是你的第三个例子(有问题的例子)。
为确保未初始化的实例未被另一个线程检索,在完全初始化实例之前,不应将其分配给静态变量。然后,一旦他们看到非空,他们就知道这很好。 已编辑 :(更正)虽然我认为避免冗余工作更为理想(下图),但我认为允许重新生成和替换静态参考(如您所知,或Guffa的版本)仍然是“安全的” “对于你的用例,因为每次引用GetMyObject()
都会抓住当时的单个对象。
我会建议这样的事情:
private static object s_Lock = new object();
private static volatile MyObject s_MyObject;
private static MyObject LoadMyObject()
{
MyObject myObject = new MyObject();
myObject.Initialize();
return myObject;
}
public static MyObject GetMyObject()
{
if (s_MyObject == null)
{
lock (s_Lock)
{
if (s_MyObject == null) // Check again now that we're in the lock.
s_MyObject = LoadMyObject(); // Only one access does this work.
}
}
return s_MyObject; // Once it's set, it's never cleared or assigned again.
}
这样做的好处是只执行一次init工作,但也避免了锁争用开销,除非它实际需要......同时仍然是线程安全的。 (你可以使用如上所述的volatile,如果你想确保它传播出去,但是如果没有它,它也应该“工作”;在看到s_MyObject
的新内容之前,它更可能需要锁定,这取决于在架构上。所以我认为volatile对这种方法很有帮助。)
我保留了LoadMyObject
和GetMyObject
方法,但是如果是单个工厂方法,你也可以将逻辑组合到内部的then子句中。
答案 1 :(得分:1)
在某种意义上说,你将获得一个正确初始化的对象是安全的,但是有几个线程可能同时创建对象,而一个线程可能会返回一个刚刚创建的另一个线程的对象。
如果编译器内联该方法,它仍然不会与第一个代码相同。由于对象的引用保存在局部变量中,直到它被初始化,它仍然是安全的。
您可以自己内联方法,不需要在单独的方法中使用代码来确保安全:
public static MyObject GetMyObject() {
if (myObject == null) {
MyObject newObject = new MyObject();
newObject.Initialize(...);
myObject = newObject;
}
return myObject;
}
如果要防止由不同的线程创建多个对象,则需要使用锁。
答案 2 :(得分:1)
要回答第二次编辑,如果初始化方法需要一段时间,如果对象不为null,则有可能另一个线程可能为k,然后拉出其未初始化状态。我建议你使用一个锁进行空检查和初始化,以保证只在完全初始化时才检索对象。
如果您希望它完全是线程安全的,这可以提供帮助:
private static MyObject myObject;
private object lockObj = new object();
...
public static MyObject GetMyObject()
{
lock(lockObj)
{
if (myObject == null)
{
myObject = new MyObject(...);
}
}
return myObject;
}
答案 3 :(得分:1)
但我会担心JITter 也许可以内联 LoadMyObject方法和本质上 重新实现它作为上面的代码, 这不是线程安全的。
即使该方法是内联的,您的本地也不会被优化掉,并且对该字段的写入仍然只会发生一次。
所以你的最后一个例子是线程安全的。
作为一项规则,当对变量有多个访问权限时,本地人不会被优化并被全局变量替换。
答案 4 :(得分:0)
如果我可以将clippy传达一分钟:“看起来你正试图实现Singleton模式”。请参阅Jon Skeet's canonical article on the subject。
答案 5 :(得分:0)
为什么不将Initialize方法移动到MyObject的c'tor并通过一次调用初始化它?
答案 6 :(得分:0)
即使没有编译器优化,它仍然不是线程安全的。在这种情况下,易失性无济于事 - 它仅用于保护变量不被“隐藏”修改。想象一下两个线程在同一时间通过空检查 - 你最终会调用两次“LoadMyObject”,在这种情况下可能没问题,或者可能没有。这是TOCTOU错误(检查时间/使用时间)。您基本上必须使整个GetMyObject方法的主体安全,包括检查(即通过使该调用同步)。
答案 7 :(得分:0)
以下是我的看法:
public class TestClass
{
private static TestClass instance = LoadObject();
private TestClass(){ }
private static TestClass LoadObject()
{
var t = new TestClass();
//do init
return t;
}
public static TestClass GetObject()
{
return instance;
}
}
答案 8 :(得分:0)
在我看来,这似乎是一种优化。而不是深入了解编译器正在做的事情的微妙之处,而是选择最合适的一个:
这一切都取决于你想要支付的罚款。对于单身人士,您需要支付性能损失。对于其他两个选项,您需要支付内存罚金。
要使其成为线程静态,请执行以下操作:
[ThreadStatic]
private static MyObject myObject;
...
public static MyObject GetMyObject()
{
if (myObject == null)
{
myObject = new MyObject(...);
}
return myObject;
}