寻找完美的单例模式
本文仅仅是关于如何实现完美的单例的内省。本文试图基于实际经验进行探索,并从普通开发者的视角进行解释。目前的思路仅适用于单线程应用程序。
单例模式
好吧,首先,单例是最常用——也是最被滥用的——设计模式之一。我为什么要说滥用?你读下去就会找到答案。
单例模式通常用于限制一个类的实例,即在软件程序生命周期内的对象数量为一。但我们为什么要使用单例呢?我们使用它们
- 来协调系统中的操作
- 来提高系统效率
在使用单例的必要性与益处方面,应仔细加以识别。我们常常倾向于使用单例来隐藏一个全局对象。
如何创建单例?
我们大多数人都知道如何创建单例。我们现在已经做了很多次了,不是吗?但我们还是快速回顾一下。
单例的黄金法则是要阻止通过构造函数进行直接实例化。那么我们如何实例化或访问对象呢?解决方案是提供一个方法,在对象不存在时创建一个新实例,如果已存在,则简单地返回该对象的引用。
C++
class Singleton {
Singleton() {
}
};
请记住,C++ 类中的默认访问权限是 private
。但对于 Java 来说,构造函数的访问修饰符继承自类。如果类上没有定义访问修饰符,则默认构造函数将具有没有访问修饰符所隐含的默认访问权限。(参考:Java 语言规范第二版 8.8.7 默认构造函数)。
这意味着在 Java 中,应特别注意将构造函数的访问修饰符明确设置为 private
。
Java
package ...;
public class Singleton {
private Singleton() {
}
};
既然我们已经阻止了类的直接实例化,我们就需要提供一个访问对象的机制。方法如下:
C++
class Singleton {
public:
static Singleton* getInstance() {
if (NULL == _instance) {
_instance = new Singleton();
}
return _instance;
}
private:
// default constructor
Singleton() {
}
// the single instance of the object
static Singleton* _instance;
};
Singleton* Singleton::_instance = NULL;
Java
public class Singleton {
public static Singleton getInstance() {
if (null == _instance) {
_instance = new Singleton();
}
return _instance;
}
// default constructor
private Singleton() {
}
// the single instance of the object
private static Singleton _instance;
};
现在我们确保只有一个实例存在并提供对该实例的访问。然后可以将其他所需功能添加到 Singleton 类中。
就这些吗?
我们完成了吗?不,还不是完全。还记得我曾说过这是创建完美单例的尝试。那么上面有什么问题呢?我们是否遗漏了什么?是的,我们遗漏了。尽管我们阻止了直接实例化,但仍然可以创建一个对象。如果你回顾基本知识,所有 C++ 类除了拥有默认构造函数外,还拥有
- 复制构造函数
- 赋值运算符
我们需要将它们也声明为 private
,以阻止访问。你可能会问为什么。我们这样做是因为
- 拷贝构造函数将从现有对象创建一个新对象。但我们只对限制实例为一感兴趣。
- 单例不需要赋值运算符;‘_instance’ 是唯一的数据成员,并且必须在所有实例中指向同一个对象。
C++
class Singleton {
public:
static Singleton* getInstance() {
if (NULL == _instance) {
_instance = new Singleton();
}
return _instance;
}
private:
// default constructor
Singleton() {
}
// copy constructor
Singleton(const Singleton&) {
}
// assignment operator
Singleton& operator=(const Singleton&) {
return *this;
}
// the single instance of the object
static Singleton* _instance;
};
Singleton* Singleton::_instance = NULL;
在 Java 中,我们倾向于忘记克隆,就像忘记拷贝构造函数一样。尽管我们的 Singleton 类没有定义 clone 方法,但我们需要这样做,因为我们 Singleton 所继承的 java.lang.Object
类定义了 clone()
方法。因此,为了使我们的 Singleton 尽善尽美,我们需要添加 clone()
方法并阻止对其的访问。由于我们不能将其设为 private
,我们可以将其设为 protected
,或者重写它并抛出异常,或者两者都做。
Java
public class Singleton {
public static Singleton getInstance() {
if (null == _instance) {
_instance = new Singleton();
}
return _instance;
}
// default constructor
private Singleton() {
}
// clone
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
// the single instance of the object private
static Singleton _instance;
};
如何释放单例?
现在我们已经阻止了创建类的另一个实例的替代方法。但如何销毁实例呢?我们可以定义一个析构函数并在其中删除 _instance
吗?这看起来非常简单,但实际上并非如此。如果你这样做,你的代码将变成
C++
class Singleton {
public:
. . .
. . .
~Singleton() {
delete _instance;
}
};
该析构函数实际上所做的是,delete _instance
再次调用析构函数,使其陷入无限循环。所以,简单的解决办法似乎是依赖于程序终止后的清理来释放内存。但依赖于程序终止后的清理来处理我们已执行的 new 操作并不是一个好习惯。因此,我建议拥有一个 releaseInstance()
方法,并通过将其设为 private
来阻止访问默认析构函数。
C++
class Singleton {
public:
static Singleton* getInstance() {
if (NULL == _instance) {
_instance = new Singleton();
}
return _instance;
}
static void releaseInstance() {
if (NULL != _instance) {
delete _instance;
_instance = NULL;
}
}
private:
// default constructor
Singleton() {
}
// default destructor
~Singleton() {
}
// copy constructor
Singleton(const Singleton&) {
}
// assignment operator
Singleton& operator=(const Singleton&) {
return *this;
}
// the single instance of the object
static Singleton* _instance;
};
Singleton* Singleton::_instance = NULL;
调整单例的释放
现在这看起来很完美了。一切都说完了,但仍然有一个陷阱。何时应调用 releaseInstance()
?理想情况下,这应该在应用程序退出时调用。但在实际情况中,当有多个开发人员在应用程序代码上工作时,这只会成为一个海市蜃楼。那么我们该怎么办?我们将使用引用计数来确保 Singleton 的实际销毁仅在不再被引用时发生。因此,我们在 Singleton 中添加一个 static
引用计数。
C++
class Singleton {
public:
static Singleton* getInstance() {
if (NULL == _instance) {
_instance = new Singleton();
}
_referenceCount++;
return _instance;
}
static void releaseInstance() {
_referenceCount--;
if ((0 == _referenceCount) && (NULL != _instance)) {
delete _instance;
_instance = NULL;
}
}
private:
// default constructor
Singleton() {
}
// default destructor
~Singleton() {
}
// copy constructor
Singleton(const Singleton&) {
}
// assignment operator
Singleton& operator=(const Singleton&) {
return *this;
}
// the single instance of the object
static Singleton* _instance;
// the count of references
static int _referenceCount;
};
int Singleton::_referenceCount = 0;
Singleton* Singleton::_instance = NULL;
这要好得多,但仍不完全万无一失。如果一个特定用户调用 releaseInstance()
的次数过多,将会出现一个实例被删除,即使它实际上仍在被使用的尴尬局面。相反,如果用户没有调用 releaseInstance()
,那么 Singleton 将永远不会被删除,我们又回到了依赖于程序终止后的清理。
就 Java 而言,据我所知,垃圾回收在实例不再被引用时会负责释放实例。因此,对于 C++ 而言,并没有出现上述预想的那么多问题。
终章
处理 C++ 中局限性的一种简单方法是通过有效的代码审查。但风险仍然存在,因为它与代码审查的有效性成正比。虽然还有另一种方法可以最小化这种风险,但总似乎有一种方法可以绕过。
我正在努力完善这个常见模式的实现。
注意
以上仅适用于单线程应用程序。我将尽快更新本文以适用于多线程应用程序。代码是使用 Eclipse 3.2 和 CDT 编写的。
历史
- 2006 年 9 月 14 日:首次发布