1. 基本概念

1.1 原理

单例模式可以说是所有设计模式中最简单的一个了,这里我们先直接给出它的概念然后再对它进行详细的讲解。单例模式就是:一个类只能有一个实例,并提供对该实例的全局访问点。通俗地说,就是一个类只能创建一个对象,并且在程序的任何地方都能够访问到该对象。

在某些情况下一些类只需要一个实例就够了,我们以一个简化的文件管理器作为例子来说明。假设该文件管理器具备如下功能:

1. 查看一个文件是否存在;

2. 创建一个文件;

3. 删除一个文件。

为了执行这些查看、创建或删除的任务,我们首先肯定要创建该文件管理器类的一个对象,然后在该对象上调用执行相应任务的方法(也叫成员函数)。假设我们可以为该类创建多个对象,那么这些对象的作用都是完全一样的(在同一台设备上执行文件相关操作,即这些对象都操作同一个文件系统)。因此,我们没有必要创建这么多个对象,只要有一个就行了。

此外,在另一些极端情况下某些类只能有一个实例;如果存在多个实例,那么在多个地方对同一组数据进行操作可能导致数据不一致等错误情况。这就是单例模式存在的原因。

因为只需要创建该类的一个实例,不需要频繁地创建和销毁多个实例,因此单例模式可以节省内存并提高性能。不仅如此,它还让程序代码组织得更好,更简单易读。

1.2 结构

图1 单例模式的类图

在文章开篇我们就说了单例模式是最简单的设计模式,因此它的类图也是相当的简单,整个类图中就只有一个类,如图1所示。

这个唯一的类就是要实现为单例模式的类,你可以给它取任意的名字,这里显示为Singleton仅仅是为了表明它是一个单例类而已。为了简单起见,该类图只显示了该类的一个私有静态成员变量、一个公有静态方法(又叫静态成员函数)以及一个私有的构造函数,它们的作用会在后面的实现部分进行说明。

提示,在UML类图中成员前面的减号(-)表示私有(private)、加号(+)表示公有(public)以及井号(#)表示受保护的(protected)。

1.3 实现

既然知道了单例模式的原理,我们就要考虑如何实现它了。根据它的概念,我们要实现单例模式就是要解决两个问题:

1. 该类只能创建一个实例;

2. 该实例全局可访问。

首先为了满足第一个问题,我们必须让该类的构造函数是protected或private修饰的,绝对不能为public。否则,用户可以自行创建该类的多个实例,因为他可以直接访问该类的构造函数。即便我们在文档中指明该类只应该创建一个实例,他们也很可能不会遵守。

当一个类的构造函数为protected或private的时候,它自身是可以访问这些构造函数的,所以我们必须让单例类自己创建自己的实例。因此我们必须为该类提供一个静态方法,因为静态方法可以直接在类上调用、即便此时还不存在该类的实例。从上面的类图中我们可以看到单例类有一个名为getInstance的方法,它就被标记为静态的和公有的。

该静态方法还需要保证它只会创建一个实例,它可以这样做:先检查是否已创建了该单例类的一个实例,如果有就直接返回该实例,如果没有就先创建一个新实例再返回该实例。

再来看第二个问题,初看我们可以提供一个全局变量来保存该单例类的唯一实例。这确实可以提供全局访问点,但是该全局变量可能被用户赋予其它的值,这就造成了程序的混乱。

既然,我们都已经让该单例类自己创建它的实例了,那么为什么也不让它自己负责保存自己的实例呢?我们再为该类提供一个静态成员变量(类图中名为instance),用它来保存该类的唯一实例。注意该静态成员变量必须是private的,以防止用户可以直接访问到它。如果用户想要访问该单例类的唯一实例,它只能调用该类的静态方法(getInstance)。

由此可见,该静态成员变量、静态方法和私有构造函数对实现单例模式至关重要。所以为了简单起见,在图1所示的类图中只显示了这三个成员,但你需要明白该类应该还有其它的非静态的成员变量和方法。当获取到该类的唯一实例后,就在该实例上调用这些其它方法来执行该类提供的功能。

2. 示例

关于单例模式的所有知识都讲解得差不多了,虽然看似文字比较多,但其实很简单。现在,我们就通过实际的代码来演示单例模式。首先,我们用单例模式实现一个名为FileManager的类,它的代码如下所示。

使用该单例类的用户代码如下所示:

对上面的实现方式还有一点需要说明,那就是该实现方式叫做懒汉式。这是因为只有在第一次调用getInstance方法的时候才会创建该单例类的唯一实例,而在这之前程序中都没有该类的实例存在。这种延迟创建的方式的好处是它可以节省内存占用,如果在程序的整个执行期间都不需要访问该单例类的实例,那么在整个程序生命期中都不会创建该实例。

但是,这种懒汉式也有一个缺点,那就是它不是线程安全的。比如有两个线程A和B交替执行,A先执行并且它判断出instance为null。然后切换到线程B,此时它也判断出instance为null,所以它会实例化该单例类并赋值给instance变量。这个时候A线程恢复执行,因为它之前已判断了instance为null,所以它也会实例化该单例类并赋值给instance变量。在我们这个足够简单的例子中看起来这没有多大问题,但在更复杂的真实环境中它可能带来致命的错误。

可以通过线程同步来解决这一问题,比如锁机制;如果使用的是Java语言的话,也可以使用它的synchronized关键字,synchronized关键字的作用和锁机制一样,它自动为一个方法提供线程同步的功能。采用了synchronized关键字的Java代码如下所示:

引入了synchronized关键字后,虽然解决了问题但会导致程序运行变慢。在Java中单例模式还有一种实现方式叫做饿汉式,它在加载该单例类的时候就创建它的唯一实例,那么在getInstance方法中就只需返回该实例即可,无需再判断instance变量是否为null。但是这样的话,即便在整个程序生命周期中都没有使用该单例类,它的实例也被创建了,会占用一部分内存空间。饿汉式的实现代码如下:

(完)