设计模式:单例模式(C#、JAVA、JavaScript、C++、Python、Go、PHP)

大家好!本节主要介绍设计模式中的单例模式。

简介:

单例模式,它是一种常用的软件设计模式,它属于创建类型。单例模式的主要目的是确保一个类仅有一个实例,并提供一个全局访问点。

在单例模式中,一个类只有一个实例,而且自行实例化并向整个系统提供。通常,单例模式的实现会使用一个私有静态变量来保存类的唯一实例,并提供一个公共静态方法来获取该实例。这个方法通常是懒加载的,即在第一次调用时才创建实例。

单例模式的使用场景:

1、配置文件的读取和存储:在项目中,通常需要读取和存储配置信息。使用单例模式可以避免频繁地创建和销毁配置对象,节省开销。

2、线程池的设计:在多线程应用中,线程池的设计可以使用单例模式。这样可以方便地对池中的线程进行控制。

3、日志应用:在应用程序中,日志是一个重要的部分。使用单例模式可以避免重复创建日志对象,提高性能。

4、数据库连接池:在数据库系统中,连接池的设计通常采用单例模式。这是因为数据库连接是一种珍贵的资源,重复创建和销毁连接会带来很大的性能开销。

5、网站的计数器:网站计数器是一个需要单例的场景。如果每个请求都创建一个计数器对象,会导致系统资源的浪费。使用单例模式可以节省资源。

需要注意的是,单例模式虽然有用,但也要谨慎使用,过度使用会导致代码的耦合度过高,不利于维护和扩展。

单例模式的创建步骤:

1、将构造函数的访问权限设置成private或者protected,从而不允许类的外部创建对象,保证对象数目只有一个。

2、用一个静态成员指针指向创建的对象。

3、提供了一个静态成员函数,用来获取这个对象的首地址。

单例模式的优点,主要包括:

1、保证所有对象都访问唯一实例。单例模式确保了一个类只有一个实例,避免了系统中出现多个相同实例的情况,提高了系统的可靠性。

2、减少内存开支和系统的性能开销。由于单例模式只创建一个对象实例,因此可以节省系统资源和提高系统性能。对于需要频繁创建和销毁的对象,单例模式可以提高系统的性能。

2、提供对唯一实例的受控访问。单例模式封装了它的唯一实例,可以严格控制客户怎样以及何时访问它,使得类的实例化过程可控。

4、可以扩展到多例模式。基于单例模式,可以通过单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。

单例模式的缺点,主要包括:

1、系统开销。虽然单例模式可以节省内存和系统性能,但是每次引用这个类的时候都要进行实例是否存在的检查,这会增加系统的开销。

2、开发混淆。当使用一个单例模式的对象的时候,特别是定义在类库中的,开发人员必须记住不能使用new关键字来实例化对象。如果开发者看不到在类库中的源代码,他们可能会对此感到惊讶。

3、对象生命周期。单例模式没有提出对象的销毁,这可能导致内存泄漏。在提供内存管理的开发语言中,只有单例模式对象自己才能将对象实例销毁,因为只有它拥有对实例的引用。在各种开发语言中,如C++,其它类可以销毁对象实例,但这将导致单例类内部的指针指向不明。

4、扩展性不佳。单例模式是基于一种先实例化、再使用的模式,对于需要动态扩展的场景并不十分适用。

5、无法被继承。单例模式的单例对象无法被子类继承,也就无法使用子类来替换父类的实例,这在一定程度上限制了代码的灵活性和可重用性。

总之,虽然单例模式可以带来一些好处,但也不能滥用单例模式,需要考虑系统的实际需求和上下文环境,根据具体情况灵活选择是否使用单例模式。

示例:

一、C#单例模式

在C#中,我们可以使用静态构造函数和静态成员变量来实现单例模式。

cs 复制代码
public class Singleton  
{  
    // 静态构造函数在类被任何静态成员使用之前自动调用  
    static Singleton()  
    {  
        // 可以在这里进行一些初始化操作  
    }  
  
    // 私有静态实例变量,以便在类外部无法访问到它  
    private static readonly Singleton instance = new Singleton();  
  
    // 私有构造函数,以便在类外部无法访问到它  
    private Singleton()  
    {  
    }  
  
    // 公共静态方法,用于访问该实例  
    public static Singleton Instance  
    {  
        get { return instance; }  
    }  
}

但,这个方法存在多线程并发问题。为了解决多线程并发问题,可以使用线程安全来保证单例模式的实现。以下是一种线程安全的单例模式实现方法:

cs 复制代码
public sealed class Singleton  
{  
    private static readonly Lazy<Singleton> _instance = new Lazy<Singleton>(() => new Singleton());  
  
    private Singleton()  
    {  
        // Private constructor to prevent instantiation from outside the class.  
    }  
  
    public static Singleton Instance  
    {  
        get { return _instance.Value; }  
    }  
}

上述代码中,使用了Lazy<T>类型来延迟初始化单例实例,直到第一次访问Instance属性时才创建实例。由于Lazy<T>是线程安全的,因此可以在多线程环境下正确地实现单例模式。

cs 复制代码
//要使用这个Singleton实例,你可以像这样访问:
Singleton singleton = Singleton.Instance;

注意,在实现单例模式时,应该避免在构造函数中进行复杂的操作或依赖其他尚未初始化的依赖项,以避免潜在的问题。

除了上述实现方法,还可以使用Mutex、Monitor或volatile关键字等其他方式来实现线程安全的单例模式。但使用Lazy<T>是较为简洁和高效的一种实现方式。

二、java单例模式

在Java中实现单例模式有多种方法,下面给出两种常用的方法:

1、饿汉式:

这种方法是在类加载的时候就已经创建了实例,因此也称为"饿汉式"单例模式。它适用于在类加载时就需要使用到该实例的场景。

java 复制代码
public class Singleton {  
    private static Singleton instance = new Singleton();  
  
    private Singleton() {}  
  
    public static Singleton getInstance() {  
        return instance;  
    }  
}

2、懒汉式:

这种方法是在需要使用实例的时候才会创建,因此也称为"懒汉式"单例模式。它适用于在类加载时不需要使用到该实例,而在运行时才需要的场景。

java 复制代码
public class Singleton {  
    private static Singleton instance;  
  
    private Singleton() {}  
  
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

在上面的代码中,getInstance()方法使用了synchronized关键字来实现线程同步,以避免在多线程环境下创建多个实例。但是这种方法会造成性能上的开销,因此也可以使用双重检查锁定(Double-Checked Locking)机制来提高性能:

3、双重检查锁定

java 复制代码
public class Singleton {  
    private static volatile Singleton instance;  
  
    private Singleton() {}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            synchronized (Singleton.class) {  
                if (instance == null) {  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}

在上面的代码中,使用volatile关键字来保证,在instance变量被初始化为非null值之前,所有线程都看到该变量的值被初始化为null,这是JMM(Java Memory Model)的Memory OrderEffects中volatile建立happens-before关系的一个应用。这样就可以避免在第一次创建实例时进行同步,从而提高性能。

三、javascript单例模式

在JavaScript中,以下是一个基本的JavaScript单例模式的例子:

javascript 复制代码
class Singleton {  
  constructor() {  
    if (typeof Singleton._instance !== 'undefined') {  
      throw new Error('Singleton class already has a instance!');  
    }  
  
    this._privateProperty = 'Hello, this is a private property!';  
    Singleton._instance = this;  
  }  
  
  get instance() {  
    return Singleton._instance;  
  }  
  
  publicMethod() {  
    console.log('Hello from a public method!');  
  }  
}  
  
// 使用单例  
const instance1 = Singleton.instance;  
const instance2 = new Singleton(); // 实际上创建了一个新的实例,但是...  
const instance3 = Singleton.instance; // 仍然是同一个实例  
  
console.log(instance1 === instance3); // 输出:true  
instance1.publicMethod(); // 输出:Hello from a public method!

在这个例子中,通过创建一个类,并在其构造函数中进行检查,我们确保了只有一个实例被创建。我们还定义了一个instance属性,它返回该类的唯一实例,以及一个公共方法publicMethod。

然而,这个实现方法有一个小问题:如果我们尝试通过new Singleton()来创建新的实例,虽然实际上并没有创建新的实例,但是Singleton._instance的值却被改变了。这可能导致一些意想不到的副作用。

有一种更优雅的实现单例模式的方式,那就是使用"自执行函数":

javascript 复制代码
let Singleton = (function () {  
  let instance;  
  
  function createInstance() {  
    let object = new Object("I am the instance");  
    return object;  
  }  
  
  return {  
    getInstance: function () {  
      if (!instance) {  
        instance = createInstance();  
      }  
      return instance;  
    }  
  };  
})();  
  
// 使用单例  
const instance1 = Singleton.getInstance();  
const instance2 = Singleton.getInstance();  
  
console.log(instance1 === instance2); // 输出:true

在这个版本中,我们通过创建一个自执行函数来定义了一个对象,该对象只有一个属性getInstance,该属性是一个返回实例的函数。只有当实例不存在时,getInstance才会创建新实例。因此,每次调用Singleton.getInstance()时,都会返回同一个实例。这种方式实现单例模式更为优雅,也更安全。

四、C++单例模式

在C++中实现单例模式有多种方式,但最常见的方式是通过在类中定义一个静态成员变量和一个静态成员函数。以下是一种常见的实现方式:

cpp 复制代码
class Singleton {  
public:  
    static Singleton& getInstance() {  
        static Singleton instance;  
        return instance;  
    }  
  
    // 防止拷贝构造函数和赋值运算符被调用,从而保证单例的唯一性  
    Singleton(Singleton const&) = delete;  
    void operator=(Singleton const&) = delete;  
  
private:  
    Singleton() {}  
};

这种方式的好处是简单且线程安全。在C++11及以上版本中,静态局部变量的初始化是线程安全的。因此,即使多个线程同时调用getInstance(),也只会创建一个实例。

这个实现方式还通过将构造函数私有化,防止其他地方创建新的实例。同时,将拷贝构造函数和赋值运算符设为删除状态,防止已有的实例被复制或赋值,从而保证了单例的唯一性。

如果你使用的是C++11及以上版本,可以进一步简化代码,使用std::call_once和std::promise来实现线程安全的单例模式:

cpp 复制代码
#include <once_flag.h>  
#include <future_utils.h>  
  
class Singleton {  
public:  
    static std::future<Singleton&> getInstance() {  
        std::call_once(initFlag, &Singleton::init, nullptr);  
        return future<Singleton&>(futureFlag.get_promise().get_future());  
    }  
  
private:  
    static void init() {  
        singletonInstance = Singleton();  
    }  
  
    Singleton() {}  
    Singleton(const Singleton&) = delete;  
    void operator=(const Singleton&) = delete;  
  
    static std::once_flag initFlag;  
    static Singleton& singletonInstance;  
    static std::promise<Singleton&> futureFlag;  
};

这个实现方式使用了C++11中的std::call_once和std::promise,保证了在多线程环境中也只会初始化一次实例。需要注意的是,这种方式需要包含头文件<once_flag.h>和<future_utils.h>,这两个头文件在C++17及以上版本中是标准库的一部分。

五、python单例模式

在 Python 中实现单例模式有多种方式,下面介绍其中两种常见的方法。

1、使用模块变量

我们可以将单例实例作为模块变量,在模块被导入时初始化,并通过一个静态方法来获取实例。

python 复制代码
# singleton.py  
class Singleton:  
    _instance = None  
  
    @staticmethod  
    def getInstance():  
        if Singleton._instance is None:  
            Singleton._instance = Singleton()  
        return Singleton._instance

使用时,我们可以从该模块中导入Singleton类,并调用getInstance()方法获取单例实例。

python 复制代码
from singleton import Singleton  
  
# 获取单例实例  
s1 = Singleton.getInstance()  
s2 = Singleton.getInstance()  
print(s1 is s2)  # True

2、使用元类

元类是创建类的类,可以通过定义一个元类来控制类的创建过程,确保只能创建一个实例。在 Python 中,所有类都是type类的实例,我们可以通过重写type类的__call__方法来实现单例模式。

python 复制代码
# singleton.py  
class SingletonType(type):  
    _instances = {}  
  
    def __call__(cls, *args, **kwargs):  
        if cls not in cls._instances:  
            cls._instances[cls] = super().__call__(*args, **kwargs)  
        return cls._instances[cls]  
  
class Singleton:  
    __metaclass__ = SingletonType

使用时,我们可以直接创建Singleton类的实例,每次创建实例时都会返回同一个对象。

python 复制代码
from singleton import Singleton  
  
# 获取单例实例  
s1 = Singleton()  
s2 = Singleton()  
print(s1 is s2)  # True	

总的来说,使用元类的方式更加简洁和直观,但使用模块变量方式更加常见,因为它不需要重写类创建过程,同时也能保证线程安全。

六、go单例模式

在Go语言中实现单例模式可以使用以下方式之一:

1、懒汉模式:

这是最基本的单例模式实现方式,但它在多线程环境下是不安全的。

Go 复制代码
type Singleton struct {  
    // 单例对象的私有字段  
}  
  
var instance *Singleton  
  
func GetInstance() *Singleton {  
    if instance == nil {  
        instance = &Singleton{} // 创建单例对象  
    }  
    return instance  
}

在多线程环境下,如果两个线程同时调用GetInstance函数,可能会创建两个实例。

2、饿汉模式:

这种模式在程序启动时就创建了单例对象,所以称为"饿汉模式"。

Go 复制代码
type Singleton struct {  
    // 单例对象的私有字段  
}  
  
var instance *Singleton = &Singleton{} // 创建单例对象  
  
func GetInstance() *Singleton {  
    return instance  
}

这种模式下,单例对象在程序启动时就被创建,所以不会存在创建多个实例的情况。

3、双重检查锁定(Double-Checked Locking):

这是在懒汉模式和饿汉模式的基础上进行的改进,它通过在懒汉模式下增加了同步锁来保证在多线程环境下只创建一个实例。

Go 复制代码
type Singleton struct {  
    // 单例对象的私有字段  
}  
  
var instance *Singleton  
var once sync.Once  
  
func GetInstance() *Singleton {  
    once.Do(func() {  
        instance = &Singleton{} // 创建单例对象  
    })  
    return instance  
}

在第一次调用GetInstance时,once.Do会执行传入的函数,也就是创建单例对象。之后的调用不会再执行该函数,直接返回已创建好的实例。使用sync.Once可以保证即使在多线程环境下,也只执行一次创建实例的操作。

七、PHP单例模式

在PHP中实现单例模式有两种方式:

1、使用互斥锁(Mutex):在创建单例实例之前,使用互斥锁确保只有一个线程可以进入临界区。你可以使用PHP的扩展库提供的互斥锁函数(例如,sem_get()和sem_acquire())来同步线程。只有获得互斥锁的线程才能创建单例实例,其他线程则等待直到锁被释放。

php 复制代码
class Singleton {  
    private static $instance;  
    private static $mutex;  
  
    private function __construct() {  
        // 私有构造函数  
    }  
  
    public static function getInstance() {  
        if (self::$instance === null) {  
            // 获取互斥锁  
            if (!self::$mutex) {  
                self::$mutex = sem_get(sem_count() + 1, 1);  
                sem_acquire(self::$mutex);  
            }  
  
            // 创建单例实例  
            self::$instance = new self();  
  
            // 释放互斥锁  
            sem_release(self::$mutex);  
        }  
  
        return self::$instance;  
    }  
}

2、使用静态初始化器(Static Initializer):在PHP 7及以上版本中,你可以使用静态初始化器来确保单例实例只被创建一次。静态初始化器是一个在类加载时执行的方法,可以在该方法中创建单例实例。

php 复制代码
class Singleton {  
    private static $instance;  
  
    private function __construct() {  
        // 私有构造函数  
    }  
  
    public static function getInstance() {  
        if (self::$instance === null) {  
            self::$instance = new self();  
        }  
  
        return self::$instance;  
    }  
  
    // 静态初始化方法  
    public static function __clone() { }  
}

静态初始化器的__clone()方法在尝试克隆单例实例时被调用,并且什么也不做,从而阻止了通过克隆来创建新的实例。同时,使用静态初始化器时,PHP会保证类只被加载一次,因此单例实例也只会被创建一次。

这些方法可以帮助你在多线程环境下处理并发访问问题,但需要注意的是,这些方法并不能完全保证线程安全。在并发情况下,还可能存在其他并发访问问题,例如延迟初始化问题。为了确保完全线程安全,你可以考虑使用其他设计模式,如工厂模式或读写锁(ReadWrite Lock)等。

《完结》