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# 双检锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C#面向对象设计原则之组合/聚合复用原则

    C#面向对象设计原则之组合/聚合复用原则

    这篇文章介绍了C#面向对象设计原则之组合/聚合复用原则,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-03-03
  • C#静态构造函数用法实例分析

    C#静态构造函数用法实例分析

    这篇文章主要介绍了C#静态构造函数用法,以实例形式较为详细的分析了C#静态构造函数的用途、实现方法及使用技巧,需要的朋友可以参考下
    2015-06-06
  • C#中winform使用相对路径读取文件的方法

    C#中winform使用相对路径读取文件的方法

    这篇文章主要介绍了C#中winform使用相对路径读取文件的方法,实例分析了C#使用相对路径读取文件的技巧与实际应用,需要的朋友可以参考下
    2015-01-01
  • C#确保只有一个实例在运行的方法

    C#确保只有一个实例在运行的方法

    这篇文章主要介绍了C#确保只有一个实例在运行的方法,涉及C#进程操作的相关技巧,需要的朋友可以参考下
    2015-05-05
  • webBrowser代理设置c#代码

    webBrowser代理设置c#代码

    本文将介绍C# 为webBrowser设置代理实现代码,需要了解的朋友可以参考下
    2012-11-11
  • C#使用LibUsbDotNet实现USB设备检测

    C#使用LibUsbDotNet实现USB设备检测

    C# LibUsbDotNet是一个.NET平台上的库,用于访问USB设备,它封装了Linux下的libusb库,LibUsbDotNet提供了一个简单的API,开发者可以利用这个库在C#中操作USB设备,本文介绍了C#使用LibUsbDotNet实现USB设备检测,需要的朋友可以参考下
    2024-07-07
  • C#简单嵌套flash读取数据的实现代码

    C#简单嵌套flash读取数据的实现代码

    这篇文章主要介绍了C#简单嵌套flash读取数据的实现代码,有需要的朋友可以参考一下
    2013-11-11
  • C#判断字符串中内容是否为纯数字的详细教程

    C#判断字符串中内容是否为纯数字的详细教程

    在进行C#编程时候,有的时候我们需要判断一个字符串是否是数字字符串,下面这篇文章主要给大家介绍了关于C#判断字符串中内容是否为纯数字的详细教程,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-04-04
  • C#中struct, class的使用区别

    C#中struct, class的使用区别

    类和结构体是C#中用于定义数据类型的关键字,它们在内存分配、生命周期管理、性能等方面存在显著差异,下面就来详细的介绍一下这两者的区别,感兴趣的可以了解一下
    2026-01-01
  • Unity3D Shader实现动态屏幕遮罩

    Unity3D Shader实现动态屏幕遮罩

    这篇文章主要为大家详细介绍了Unity3D Shader实现动态屏幕遮罩效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-02-02

最新评论