使用双重检查锁定技术保证多线程中单例模式的线程安全
前言
单例模式是一种设计模式,保证一个类只有一个实例,并且在整个应用中共享。它适用于需要控制对共享资源的访问,例如数据库连接、配置文件或日志记录器。
但是,在多线程环境下实现单例模式可能比较棘手。如果多个线程同时尝试创建单例实例,我们可能会得到多个实例,这违反了单例原则。因此,我们需要保证单例创建是线程安全的,也就是说只有一个线程能够创建实例,其他线程则需要等待实例可用再获取实例。
一种常见的在多线程中实现单例模式线程安全的方法是使用互斥机制,例如锁,来保护创建单例实例的关键代码段。然而,使用互斥会带来性能开销和潜在的死锁风险。而且,不需要对每次访问单例实例的过程都进行互斥,因为大多数情况下实例已经创建好了,只需要返回它。
这时候,双重检查锁定技术就派上用场了。这是一种实现线程安全单例模式的方法。
双重检查锁定技术
双重检查锁定的思想是在创建单例实例之前,先检查两次是否为 null。第一次检查不使用任何同步手段,第二次检查则在一个互斥块中进行。代码结构如下:
if (singleton == null) // 第一次检查
{
synchronize (lock) // 获取锁
{
if (singleton == null) // 第二次检查
{
singleton = new Singleton(); // 创建实例
}
}
release (lock) // 释放锁
}
return singleton; // 返回实例
这种技术的逻辑如下:
- 如果单例实例不为 null,我们可以直接返回它,无需进行互斥。这避免了不必要的锁,也提高了性能。
- 如果单例实例为 null,我们需要获取一个锁进入一个互斥块。这保证了只有一个线程能够一次创建实例。
- 在互斥块中,我们需要再次检查单例实例是否为 null。这是因为另一个线程可能在我们获取锁之前已经创建了实例。如果仍然为 null,我们可以安全地创建一个新的实例。否则,我们可以直接返回已存在的实例。
- 在创建或返回实例之后,我们需要释放锁并退出互斥块。这允许其他线程访问单例实例。
通过使用双重检查锁定,我们可以实现线程安全、最少互斥及做到高性能。但是,在不同的编程语言中实现这种技术时,我们需要注意一些注意事项和挑战。
在 C# 中使用双重检查锁定技术
在 C# 中,实现双重检查锁定需要将单例字段声明为 volatile ,并且使用私有构造函数。
volatile 关键字保证了单例字段在所有线程中可见和一致。没有 volatile 的话,有可能一个线程看到的是一个部分构造的单例类的实例,因为编译器重排序或内存缓存的原因。这可能导致意外的错误或程序崩溃。
一般来说,volatile 适用于变量的读写操作不依赖于其他变量的情况,而 synchronized 适用于多个线程对同一个对象进行读写操作的情况。
因为 volatile 关键字只能保证单个变量的原子性,不能保证复合操作的原子性。例如,对于 i++ 这样的自增操作,如果 i 是 volatile 的,那么只能保证读取 i 和写入 i 的原子性,不能保证整个自增操作的原子性。要实现复合操作的原子性,需要使用同步机制或原子类。
私有构造函数阻止了其他类创建新的单例类的实例。没有私有构造函数的话,其他类可能通过反射或序列化绕过单例逻辑并创建多个实例。
在 C# 中双重检查锁定的完整代码如下:
public class Singleton {
// 将单例字段声明为 volatile
private static volatile Singleton singleton;
// 使用私有构造函数
private Singleton() {}
// 使用公共静态方法获取或创建单例实例
public static Singleton GetInstance() {
if (singleton == null) { // 第一次检查
lock (typeof(Singleton)) { // 获取锁
if (singleton == null) { // 第二次检查
singleton = new Singleton(); // 创建实例
}
}
}
return singleton; // 返回实例
}
}
标签:singleton,null,互斥,实例,线程,单例,多线程
From: https://www.cnblogs.com/netlog/p/17473062.html