14人参与 • 2025-12-01 • Asp.net
为什么要使用双重检查锁定(double-checked locking, dcl)”?答案的核心在于:在保证线程安全的前提下,尽可能提高性能。
下面我们从背景、问题、解决方案三个层面来解释。
在多线程程序中,如果多个线程同时调用 getinstance(),而实例尚未创建,就可能出现 多个线程同时进入 if (instance == null) 判断,从而创建多个实例 —— 这违反了单例的“唯一性”原则。
所以,必须保证线程安全。
最直接的线程安全方案是给整个 getinstance() 方法加锁:
public static singleton instance
{
get
{
lock (lockobj)
{
if (instance == null)
instance = new singleton();
return instance;
}
}
}
✅ 安全
❌ 但每次调用都要加锁!即使实例早已创建,后续所有访问仍要竞争锁,性能开销大。
在高并发场景下,这会成为明显的性能瓶颈。
目标:只在“第一次创建实例时”加锁,之后直接返回已有实例,避免无谓的同步开销。
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 等待后拿到锁,又创建一次!破坏单例!
✅ 所以,“双重检查”是为了:
在 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 足够。
虽然 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;
}
}
}
✅ 安全
❌ 但每次调用都要加锁!即使实例早已创建,后续所有访问仍要竞争锁,性能开销大。
在高并发场景下,这会成为明显的性能瓶颈。
目标:只在“第一次创建实例时”加锁,之后直接返回已有实例,避免无谓的同步开销。
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 等待后拿到锁,又创建一次!破坏单例!
✅ 所以,“双重检查”是为了:
在 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 足够。
虽然 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# 双检锁内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论