C#使用双检锁的示例代码

 更新时间:2025年12月01日 15:37:48   作者:她说彩礼65万  
本文主要介绍了C#使用双检锁的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

为什么要使用双重检查锁定(Double-Checked Locking, DCL)”?答案的核心在于:在保证线程安全的前提下,尽可能提高性能。

下面我们从背景、问题、解决方案三个层面来解释。

一、背景:单例模式 + 多线程环境

在多线程程序中,如果多个线程同时调用 getInstance(),而实例尚未创建,就可能出现 多个线程同时进入 if (instance == null) 判断,从而创建多个实例 —— 这违反了单例的“唯一性”原则。

所以,必须保证线程安全。

二、简单加锁的问题:性能瓶颈

最直接的线程安全方案是给整个 getInstance() 方法加锁:

public static Singleton Instance
{
    get
    {
        lock (lockObj)
        {
            if (instance == null)
                instance = new Singleton();
            return instance;
        }
    }
}

✅ 安全
❌ 但每次调用都要加锁!即使实例早已创建,后续所有访问仍要竞争锁,性能开销大。

在高并发场景下,这会成为明显的性能瓶颈。

三、双重检查锁定(DCL)的思路

目标:只在“第一次创建实例时”加锁,之后直接返回已有实例,避免无谓的同步开销。

实现逻辑:

  1. 第一次检查(无锁):如果 instance != null,直接返回(绝大多数情况走这里,快!)。
  2. 如果 instance == null,说明可能需要创建,此时加锁。
  3. 第二次检查(有锁):再次判断 instance == null,防止多个线程在第一次检查后都进入临界区,导致重复创建。
public static Singleton Instance
{
    get
    {
        if (instance == null)               // 第一次检查(无锁)
        {
            lock (lockObj)
            {
                if (instance == null)       // 第二次检查(有锁)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

四、为什么需要“两次”检查?

假设只有一次检查(只在锁内判断):

lock (lockObj)
{
    if (instance == null)
        instance = new Singleton();
}

→ 这样虽然安全,但每次都要加锁,失去了懒加载的性能优势。

而如果只在锁外检查一次:

if (instance == null)
{
    lock (lockObj)
    {
        instance = new Singleton(); // ❌ 没有第二次检查!
    }
}

→ 问题:线程 A 和 B 同时通过 if (instance == null),A 先拿到锁创建了实例,B 等待后拿到锁,又创建一次!破坏单例!

✅ 所以,“双重检查”是为了:

  • 避免重复创建(第二次检查)
  • 避免不必要的加锁(第一次检查)

五、内存可见性问题:为什么需要volatile

在 C#、Java 等语言中,对象创建不是原子操作,可能被重排序:

instance = new Singleton();
// 实际可能分解为:
// 1. 分配内存
// 2. 初始化对象
// 3. 将引用赋值给 instance

但编译器或 CPU 可能将步骤 2 和 3 重排序 → 先赋值引用,再初始化!

结果:线程 A 还没初始化完,线程 B 就看到 instance != null,拿到一个未完全构造的对象,导致程序崩溃!

✅ 解决方案:使用 volatile 关键字(C# / Java)

  • 禁止指令重排序
  • 保证内存可见性(一个线程写入,其他线程立即可见)
private static volatile Singleton instance;

⚠️ 在 C++ 中需使用 std::atomic 或内存屏障;在 .NET 中 volatile 足够。

六、现代替代方案:Lazy<T>

虽然 DCL 是经典解法,但在现代 C# 中,更推荐使用 Lazy<T>:

private static readonly Lazy<Singleton> lazy = 
    new Lazy<Singleton>(() => new Singleton());

public static Singleton Instance => lazy.Value;
  • 内部已实现线程安全的双重检查逻辑
  • 代码更简洁、不易出错
  • 性能同样优秀

但理解 DCL 的原理仍然非常重要,尤其在面试、底层开发或跨语言场景中。

总结:为什么要用双检锁?

目标说明
✅ 线程安全防止多线程创建多个实例
✅ 懒加载实例只在首次使用时创建
✅ 高性能实例创建后,后续访问无需加锁
✅ 避免重排序风险配合 volatile 保证正确性

💡 一句话总结:双重检查锁是在“懒加载 + 线程安全 + 高性能”三者之间取得的最佳平衡(在没有 Lazy<T> 的年代尤其重要)。

如果你在写 C#,可以直接用 Lazy<T>;但理解 DCL 背后的并发思想,对成为一名优秀开发者至关重要。这是一个非常好的问题!“为什么要使用双重检查锁定(Double-Checked Locking, DCL)”?答案的核心在于:在保证线程安全的前提下,尽可能提高性能。

下面我们从背景、问题、解决方案三个层面来解释。

一、背景:单例模式 + 多线程环境

在多线程程序中,如果多个线程同时调用 getInstance(),而实例尚未创建,就可能出现 多个线程同时进入 if (instance == null) 判断,从而创建多个实例 —— 这违反了单例的“唯一性”原则。

所以,必须保证线程安全。

二、简单加锁的问题:性能瓶颈

最直接的线程安全方案是给整个 getInstance() 方法加锁:

public static Singleton Instance
{
    get
    {
        lock (lockObj)
        {
            if (instance == null)
                instance = new Singleton();
            return instance;
        }
    }
}

✅ 安全
❌ 但每次调用都要加锁!即使实例早已创建,后续所有访问仍要竞争锁,性能开销大。

在高并发场景下,这会成为明显的性能瓶颈。

三、双重检查锁定(DCL)的思路

目标:只在“第一次创建实例时”加锁,之后直接返回已有实例,避免无谓的同步开销。

实现逻辑:

  1. 第一次检查(无锁):如果 instance != null,直接返回(绝大多数情况走这里,快!)。
  2. 如果 instance == null,说明可能需要创建,此时加锁。
  3. 第二次检查(有锁):再次判断 instance == null,防止多个线程在第一次检查后都进入临界区,导致重复创建。
public static Singleton Instance
{
    get
    {
        if (instance == null)               // 第一次检查(无锁)
        {
            lock (lockObj)
            {
                if (instance == null)       // 第二次检查(有锁)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

四、为什么需要“两次”检查?

假设只有一次检查(只在锁内判断):

lock (lockObj)
{
    if (instance == null)
        instance = new Singleton();
}

→ 这样虽然安全,但每次都要加锁,失去了懒加载的性能优势。

而如果只在锁外检查一次:

if (instance == null)
{
    lock (lockObj)
    {
        instance = new Singleton(); // ❌ 没有第二次检查!
    }
}

→ 问题:线程 A 和 B 同时通过 if (instance == null),A 先拿到锁创建了实例,B 等待后拿到锁,又创建一次!破坏单例!

✅ 所以,“双重检查”是为了:

  • 避免重复创建(第二次检查)
  • 避免不必要的加锁(第一次检查)

五、内存可见性问题:为什么需要volatile

在 C#、Java 等语言中,对象创建不是原子操作,可能被重排序:

instance = new Singleton();
// 实际可能分解为:
// 1. 分配内存
// 2. 初始化对象
// 3. 将引用赋值给 instance

但编译器或 CPU 可能将步骤 2 和 3 重排序 → 先赋值引用,再初始化!

结果:线程 A 还没初始化完,线程 B 就看到 instance != null,拿到一个未完全构造的对象,导致程序崩溃!

✅ 解决方案:使用 volatile 关键字(C# / Java)

  • 禁止指令重排序
  • 保证内存可见性(一个线程写入,其他线程立即可见)
private static volatile Singleton instance;

⚠️ 在 C++ 中需使用 std::atomic 或内存屏障;在 .NET 中 volatile 足够。

六、现代替代方案:Lazy<T>

虽然 DCL 是经典解法,但在现代 C# 中,更推荐使用 Lazy<T>:

private static readonly Lazy<Singleton> lazy = 
    new Lazy<Singleton>(() => new Singleton());

public static Singleton Instance => lazy.Value;
  • 内部已实现线程安全的双重检查逻辑
  • 代码更简洁、不易出错
  • 性能同样优秀

但理解 DCL 的原理仍然非常重要,尤其在面试、底层开发或跨语言场景中。

总结:为什么要用双检锁?

目标说明
✅ 线程安全防止多线程创建多个实例
✅ 懒加载实例只在首次使用时创建
✅ 高性能实例创建后,后续访问无需加锁
✅ 避免重排序风险配合 volatile 保证正确性

💡 一句话总结:双重检查锁是在“懒加载 + 线程安全 + 高性能”三者之间取得的最佳平衡(在没有 Lazy<T> 的年代尤其重要)。

到此这篇关于C#使用双检锁的示例代码的文章就介绍到这了,更多相关C# 双检锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • DataGridView控件常用属性介绍

    DataGridView控件常用属性介绍

    这篇文章介绍了DataGridView控件的常用属性,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-02-02
  • C#使用Spire.Doc实现将Word文档转换为XML

    C#使用Spire.Doc实现将Word文档转换为XML

    将 Word 文档转换为 XML 并非简单的格式转换,其背后蕴含着巨大的业务价值和技术优势,下面我们就来看看如何使用C#实现Word文档转换为XML吧
    2025-10-10
  • Unity调用打印机打印图片

    Unity调用打印机打印图片

    这篇文章主要为大家详细介绍了Unity通过调用打印机打印图片的代码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-10-10
  • c# Struct的一些问题分析

    c# Struct的一些问题分析

    在 C# 中,结构体是值类型数据结构。它使得一个单一变量可以存储各种数据类型的相关数据。struct 关键字用于创建结构体。结构体是用来代表一个记录。
    2021-06-06
  • c#通过ip获取地理信息

    c#通过ip获取地理信息

    这篇文章主要介绍了c#通过ip获取地理信息的方法,这里是通过cz88和ip138查询的
    2014-01-01
  • c#中SqlTransaction——事务详解

    c#中SqlTransaction——事务详解

    这篇文章主要介绍了c#中SqlTransaction——事务详解 ,具有一定的参考价值,有兴趣的可以了解一下。
    2016-12-12
  • 浅析C# 索引器(Indexer)

    浅析C# 索引器(Indexer)

    这篇文章主要介绍了C# 索引器(Indexer)的相关资料,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-07-07
  • WPF中TreeView控件的用法

    WPF中TreeView控件的用法

    这篇文章介绍了WPF中TreeView控件的用法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-06-06
  • C#使用Tesseract进行Ocr识别的方法实现

    C#使用Tesseract进行Ocr识别的方法实现

    本文主要介绍了C#使用Tesseract进行Ocr识别的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • c# 实现雪花分形的示例

    c# 实现雪花分形的示例

    这篇文章主要介绍了c# 实现雪花分形的示例,帮助大家更好的利用c#绘制图像,感兴趣的朋友可以了解下
    2020-10-10

最新评论