CodeQL 文档

双重检查锁不安全

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

单击以在 CodeQL 代码库中查看查询

双重检查锁定要求基础字段为 volatile,否则程序在多线程运行时可能会出现行为异常,例如计算两次字段。

建议

有几种方法可以使代码线程安全

  1. 避免双重检查锁定,只需在 lock 语句中执行所有操作。

  2. 使用 volatile 关键字使字段易失。

  3. 使用 System.Lazy 类,该类保证是线程安全的。这通常可以使代码更优雅。

  4. 使用 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;

参考

  • ©GitHub 公司
  • 条款
  • 隐私