为什么要使用双重检查锁定(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)的思路
目标:只在“第一次创建实例时”加锁,之后直接返回已有实例,避免无谓的同步开销。
实现逻辑:
- 第一次检查(无锁):如果 instance != null,直接返回(绝大多数情况走这里,快!)。
- 如果 instance == null,说明可能需要创建,此时加锁。
- 第二次检查(有锁):再次判断 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)的思路
目标:只在“第一次创建实例时”加锁,之后直接返回已有实例,避免无谓的同步开销。
实现逻辑:
- 第一次检查(无锁):如果 instance != null,直接返回(绝大多数情况走这里,快!)。
- 如果 instance == null,说明可能需要创建,此时加锁。
- 第二次检查(有锁):再次判断 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# 双检锁内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论