双重检查锁不安全¶
ID: cs/unsafe-double-checked-lock
Kind: problem
Security severity:
Severity: error
Precision: medium
Tags:
- correctness
- concurrency
- external/cwe/cwe-609
Query suites:
- csharp-security-and-quality.qls
双重检查锁定要求基础字段为 volatile
,否则程序在多线程运行时可能会出现行为异常,例如计算两次字段。
建议¶
有几种方法可以使代码线程安全
避免双重检查锁定,只需在 lock 语句中执行所有操作。
使用
volatile
关键字使字段易失。使用
System.Lazy
类,该类保证是线程安全的。这通常可以使代码更优雅。使用
System.Threading.LazyInitializer
。
示例¶
以下代码定义了一个名为 Name
的属性,该属性在首次读取该属性时调用方法 LoadNameFromDatabase
,然后缓存结果。此代码效率很高,但如果从多个线程访问该属性,则无法正常工作,因为 LoadNameFromDatabase
可能会被调用多次。
string name;
public string Name
{
get
{
// BAD: Not thread-safe
if (name == null)
name = LoadNameFromDatabase();
return name;
}
}
对此的常见解决方案是*双重检查锁定*,它在锁定互斥体之前检查存储的值是否为 null
。这样做效率很高,因为它避免了在已经为 name
分配值的情况下进行潜在的代价高昂的锁定操作。
string name; // BAD: Not thread-safe
public string Name
{
get
{
if (name == null)
{
lock (mutex)
{
if (name == null)
name = LoadNameFromDatabase();
}
}
return name;
}
}
但是,此代码不正确,因为字段 name
不是易失的,这可能会导致在某些系统上计算 name
两次。
第一个解决方案是简单地避免双重检查锁定(建议 1)
string name;
public string Name
{
get
{
lock (mutex) // GOOD: Thread-safe
{
if (name == null)
name = LoadNameFromDatabase();
return name;
}
}
}
另一个修复方法是使字段易失(建议 2)
volatile string name; // GOOD: Thread-safe
public string Name
{
get
{
if (name == null)
{
lock (mutex)
{
if (name == null)
name = LoadNameFromDatabase();
}
}
return name;
}
}
使用自动线程安全的 System.Lazy
类通常更优雅(建议 3)
Lazy<string> name; // GOOD: Thread-safe
public Person()
{
name = new Lazy<string>(LoadNameFromDatabase);
}
public string Name => name.Value;
参考¶
MSDN:Lazy<T> 类。
MSDN:在 C# 中实现单例。
MSDN 杂志:C# 内存模型的理论与实践。
MSDN,C# 参考:volatile。
维基百科:双重检查锁定。
常见弱点枚举:CWE-609。