65.9K
CodeProject 正在变化。 阅读更多。
Home

寻找完美的单例模式

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.94/5 (12投票s)

2006年9月14日

CPOL

5分钟阅读

viewsIcon

116620

本文仅仅是关于如何实现完美的单例的内省。本文试图基于实际经验进行探索,并从普通开发者的视角进行解释。目前的思路仅适用于单线程应用程序。

“设计和编程是人类活动;忘记这一点,一切都将失去。” - Bjarne Stroustrup

单例模式

好吧,首先,单例是最常用——也是最被滥用的——设计模式之一。我为什么要说滥用?你读下去就会找到答案。

单例模式通常用于限制一个类的实例,即在软件程序生命周期内的对象数量为一。但我们为什么要使用单例呢?我们使用它们

  • 来协调系统中的操作
  • 来提高系统效率

在使用单例的必要性与益处方面,应仔细加以识别。我们常常倾向于使用单例来隐藏一个全局对象。

如何创建单例?

我们大多数人都知道如何创建单例。我们现在已经做了很多次了,不是吗?但我们还是快速回顾一下。

单例的黄金法则是要阻止通过构造函数进行直接实例化。那么我们如何实例化或访问对象呢?解决方案是提供一个方法,在对象不存在时创建一个新实例,如果已存在,则简单地返回该对象的引用。

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 日:首次发布
© . All rights reserved.