转自 http://www.umlchina.com/xprogrammer/issue/6/singleton.htm
Singleton模式
在面向对象的程序中,某些类只需要一个实例。譬如,在一个窗口应用程序中,我们只需要一个主窗口。又如在一个数据库应用程序中,我们往往希望将所有的数据库连接集中于一处,并能为整个程序所使用。
最初,我们很容易考虑使用静态成员变量,但是这种语言本身提供的机制并不能阻止我们对类本身进行多次实例化。
再深入考虑,假设开始我们对某一个数据库系统只有一个连接许可,我们可以在Singleton类中控制它只允许建立一个连接对象。当数据库系统允许我们进行多个连接的时候,我们只需控制Singleton内部的连接建立方法,而外部的程序仍旧可以从这个单一点来访问它能获取的数据库连接,虽然这时候Singleton可以建立多于一个的对象。
也就是,Singleton的目的是控制类实例对象的创建,并且允许整个程序只在一点对它进行访问。Singleton本身类只能创建一个,可是它具有很好的适应性可以在情况发生变化时建立多个对象。这种特性使得Singleton模式经常用于控制对系统资源的控制。
有状态和无状态Singleton
Singleton可以有状态,假设我们需要产生一个全局有效的序列号,如1,2,3….,我们必须保证每一次对Singleton的请求均有Singleton保持状态,当下一次请求来到时,Singleton接口能提供一个连续不重复的序列号。
无状态的Singleton则类似于非面向对象编程中的实用函数,java.util包中有大量这样的类,第一次存取类和下一次存取类之间毫无关系。
实现Singleton
Singleton 类的实现首先要保证它只能有一个唯一的实例,从语言本身的角度来讲,这个实例和其他类的实例没有任何区别。
最简单的方法如下:
public class Singleton {
protected Singleton() {
// 构建函数. . .
}
public static Singleton getInstance() {
return _instance;
}
private static Singleton _instance = new Singleton();
//类的其余部分
}
如果再精雕细琢一下,我们可以实现如下:
public class Singleton {
protected Singleton() {
// 构建函数. . .
}
public static Singleton getInstance() {
if (_instance == null)
_instance = new Singleton();
return _instance;
}
private static Singleton _instance=null;
//类的其余部分
}
这里,_instance在需要的时候才被创建和保存,我们只需保证Singleton在它的首次使用之前被创建好即可。
注意我们将Singleton的构建函数声明为protected。如果Singleton的客户直接实例化一个Singleton对象,编译程序就不能通过。我们用这种方法确信实例只能被创建一次。当然,存取函数getInstance必定要被声明为static,因为客户不可能直接去实例化Singleton,所以只能通过static成员函数对这个类进行存取。存取static成员函数可以让java加载类。
如果不使用singleton,而是采用一个全局或静态对象,那么至少有以下一些缺点:
1. 不能保证静态或全局变量只被实例化一次;
2. 不管你是否使用这个变量,都需要首先将建立这个对象
Singleton类允许创建子类。但由于getInstance()是一个静态方法,所以子类不能覆盖getInstance()方法。而Singleton的作用是保证实例的创建的唯一性,所以事实上,这里的继承子类就是为了能够控制子类创建的唯一性,因此,Singleton中_instance私有成员必须用对应的子类进行实例化,而getInstance()是实现这一过程的最佳位置,下面的方法显示了使用环境变量实例化子类:
public class GenericSingleton {
protected GenericSingleton() {
}
public static GenericSingleton getInstance() {
if (_instance == null) {
String style =getEnv("style"); //getEnv没有实现,表示取环境变量
if (style.equals("son"))
_instance = new SonSingleton();
else if (style.equals("daughter"))
_instance = new DaughterSingleton();
else
_instance = new GenericSingleton();
}
return _instance;
}
private static GenericSingleton _instance=null;
}
public class SonSingleton extends GenericSingleton {
}
public class DaughterSingleton extends GenericSingleton {
}
GOF已经指出这种方法的弱点,不管何时GenericSingleton需要有一个新的子类,它的getInstance方法必须被修改。如果GenericSingleton是一个框架中的抽象工厂(Abstract Factory),那么这种情况几乎是不可避免的。所以我们可能会选择注册表的方法.
注册表的想法也很简单,我们不让getInstance方法显式定义可能的GenericSingleton和它子类的集合,而是在加入一个子类时,用对应的子类名字在一个注册表中注册它们对应的实例。如此,通过这个字符串名字和对应的子类对象之间建立映射关系。当客户应用程序调用getInstance来获取一个Singleton时,按照名字在注册表中查找,并返回此实例。
import java.util.*;
class Singleton {
public static void register(String name, Singleton singleton) {
_registry.put(name, singleton);
}
public static Singleton getInstance() {
String singletonName = getEnv("SINGLETON");
Singleton _instance = lookup(singletonName);
return _instance;
}
protected static Singleton lookup(String name) {
return (Singleton)_registry.get(name);
}
private static Map _registry = new HashMap();
};
这种实现方法让用户在"SINGLETON"环境变量中指定要获取的子类名字。那么子类何时在此注册表中注册自己呢?最合适的地方是它自己的构建函数:
class MySingleton extends Singleton {
MySingleton() {
super();
register(getClass().getName(),this);
}
}
问题的关键是如果不实例化MySingleton子类,这个构建函数就不会被执行。GOF在Design Patterns一书中指出,可以用静态实例,但在Java中我们需要在某处显式初始化。
static MySingleton theSingleton=new MySingleton();
下次,我们只要给定环境变量SINGLETON的值,就可以如下获取singleton的对应子类,例如:
public class Test {
public static void main(String[] args) {
System.out.println(Singleton.getInstance().getClass().getName());
}
}
保证Singleton的唯一性
Singleton模式依赖于它的唯一性,但是在特定的情形下,我们上面的努力并不能保证它的唯一性,我将在此列举那些已经被模式社团所发现的情况,并指出它们的解决方案:
在两个或更多的虚拟机中存在多个Singleton
在分布式计算环境如EJB,Jini或RMI中,对象的创建透明于客户程序。假设你使用一个无状态会话EJB来实现Singleton,那么在你两次不同的调用之间,EJB容器可以随时丢弃、重新生成这个无状态会话EJB.它完全可能在不同的虚拟机中重新启动这个EJB,但客户程序却丝毫不知情。甚至对实体EJB而言,在你的多次调用之间也完全可能被持久(persistent)于磁盘或数据库,然后下次在另外一个虚拟机中被加载。
因此,我们不能保证Singleton能够象在本地VM中这样,对它的成员属性和成员函数保持唯一性。
所以,如果你在这样的分布式环境中使用Singleton模式,请务必保证你的Singleton是无状态的。更进一步,用EJB来实现一个无状态的资源管理Singleton也是不适合的,这些本来就是EJB容器的责任。
由不同类加载器同时加载的多个Singleton
多层计算结构对Singleton模式有很多重要的影响。在GOF的design patterns一书中并没有对这些上下文进行分析。除了上面的分布式应用程序中多虚拟机的麻烦外,在同一个虚拟机内部还可能有多个类加载器的问题。
当两个类加载器(class loader)加载一个类时,实际上你就有此类的两个备份,在我们的Singleton情景下,这意味着每一个类都可能产生自己的Singleton实例。某些servlet引擎中运行的servlet就有这个问题,如iPlant对每个servlet都有自己的类加载器。如果有两个servlet存取同一个Singleton,就可能产生两个Singleton实例。
类加载器发生的问题可能远远超出你的想象。浏览器需要从网络上下载applet类到本地,对于每一个来自不同服务器地址的Applet类,它都使用一个独立的类加载器。类似地,Jini和RMI系统可能对它们从不同code base下载而来的类文件使用不同的类加载器。或者你可能使用定制的类加载器,那么同样的事情也会发生。
如果由不同的类加载器加载,相同名字两个类,即使包名字相同,也会被当作不同--事实上,就算它们完全一模一样也是如此。也就是,Java的命名空间除了类名、包名外,还有加载器。对于我们的Singleton而言,它们在不同的类加载器中都有各自的实例,于是就破坏了唯一性原则。
Singleton类被垃圾收集器销毁,然后被重新加载
当程序没有任何对象包容对Singleton对象的引用时,java VM会在适当时候自动销毁此Singleton对象。但是如果接着程序中另外一个对象需要Singleton引用,Singleton就会被重新加载。当一个Singleton类被垃圾收集然后又被重新加载后,就会产生一个新的Singleton实例。你可以清楚地看到,在这之前对Singleton任何静态成员的修改早已丢失,所有内容都会在第二次加载时重新初始化。
事实上,情况随着Java版本的变化,还有其它更复杂的情景出现:
Java 1.0 和 1.1中的Singleton
所有的Java标准都要求我们保持一个对singleton对象的引用,不然它就会被垃圾收集。但是,在Java1.2之前的Java标准中,垃圾收集器不但收集singleton对象,它甚至把Singleton类都收集了!!对一个类的垃圾收集也被称为“类卸载”。
实际上,Java1.0.x没有实现类卸载,所以我们的Singleton实现运行起来很正常。但是到了Java1.1.x,类卸载已经实现,然后我们就有了麻烦。
Java1.2中的Singleton
Sun听到了很多类似这样的抱怨(Collection框架的设计有时简直离谱),Sun在Java 2中对标准做了不少修改。
关于是否卸载一个类的新规则如下:
所有通过本地加载的类,系统类加载器从不卸载。
所有通过其它类加载器加载的类只在那个类加载器被卸下以后才被卸载。
就算如此,就象我们一开始所说,对象被垃圾收集的问题还是一个麻烦,我们一般会如此解决:
1. 在main()方法(或者是多线程中的run())中使用一个局部变量保持对singleton的引用。
2. 在你定义你main(run)方法的类中使用一个实例变量保持对singleton的引用
这些技术可以解决很多问题,但是如果你使用第三方库而又不能获得singleton,那么你就惨了。
所以,请记住,如果你的程序需要长时间运行,而它又频繁地重新加载类(如从一个远程类加载器而来的类)那么你一定要小心保持引用。
有意的Singleton类重载
类重载不一定只会发生在类垃圾收集之后;有时会应Java程序的请求而被重载。servlet 标准允许servlet引擎任何时候都可以这样做。当servlet引擎决定卸载一个servlet类时,它调用destroy(),然后将此servlet类丢弃。以后,servlet引擎会重新加载此servlet类,实例化servlet对象,然后调用init()进行初始化。实践中,这种卸载和重载进程可能会在一个servlet类或者JSP发生变化时出现。
类似于前两种情况,这种情况也会导致一个新加载的类。依赖于servlet引擎的不同,当老的servlet 类被卸载时,相关的类可能被丢弃或不被丢弃。所以如果一个servlet类有一个对Singleton的引用,你可能会发现有一个Singleton对象和老的servlet类相关连,另外还有一个Singleton对象和新的servlet类相关。
Servlet引擎的类加载机制因产品而异,所以Singleton的行为几乎是不可预期的,除非你对你所使用的servlet引擎的类加载机制十分清楚。
错误的同步造成多个Singleton实例
在最开始的Singleton中,我们使用lazy initialization来建立实例。也就是说,实例并非在类被加载的时候建立,而是在第一次使用时建立的。采用这种方法的最大问题可能是不顾及同步的问题,这样可能导致多个Singlton实例的产生。
这是我们最初的版本:
public class Singleton {
protected Singleton() {
// 构建函数. . .
}
public static Singleton getInstance() {
if (_instance == null)
_instance = new Singleton();
return _instance;
}
private static Singleton _instance=null;
//类的其余部分
}
因此,我们需要考虑同步的问题,下面是一种正确的解决方案:
public class Singleton {
private static Singleton _instance;
protected Singleton() {
// construct object . . .
}
// For lazy initialization
public static synchronized Singleton getInstance() {
if (_instance==null) {
_instance = new Singleton();
}
return _instance;
}
// Remainder of class definition . . .
}
在多线程环境下,如果不考虑同步问题,两个线程可能会同时去获取instance,然后就会出现两个实例。Singleton模式的最终目的是为了给用户一个统一的单一存取点,Singleton实现必须隐藏其中的各种细节问题和复杂性,所以,我们也必须将同步问题考虑在内。
如果没有使用正确的方式进行同步,多个Singleton实例的情况还是会出现,下面的解决方案只在调用构建方法时使用synchronized(this)块:
// Also an error, synchronization does not prevent
// two calls of constructor.
public static Singleton getInstance() {
if (_instance==null) {
synchronized (Singleton.class) {
_instance = new Singleton();
}
}
return _instance;
}
如果第一个线程发现_instance为空,然后它进入if之中,假设这时此线程被挂起,第二个线程也进入,它发现_instance还是为空,它也进入if之中,这时不管上一个线程是否在synchronized (Singleton.class)处被同步,也就是不管这两个线程谁能先开始往下执行,它们都将创建一个Singleton实例,从而破坏Singleton实例的唯一性。
也许你会想到可以在同步块内部在进行判断此实例是否为空,这叫做Double-Checked Locking方言,可惜的是,不管采用何种修正方法,最后都不能成功,我将在本章的最后一节详细讨论这个Idiom。唯一正确的方法是如上面黑体所示同步整个getInstance方法。
子类化Singleton产生的多个Singleton实例
在本章的最开始,我们已经描述了子类化一个Singlton所采用的方法。由于需要子类化,singleton类的构建函数用protected声明,而它的子类则将其声明为public,正如我们前面所描述的,我们需要在某个地方实例化这个子类,以保证它可以被注册到Singleton内部的注册表中。但这同时也导致其它人可以自己建立这个子类的其它实例。
一个被要求建立多个对象的工厂创建多个Singleton实例
我们之所以不直接使用静态方法而采用Singleton模式的重要原因之一是,Singleton模式除了能够控制唯一的存取点之外,它在你需要多个对象时能够改变Singleton的内部实现而不涉及客户代码。
例如,许多servlet在它们的servlet引擎内作为Singleton运行。因为这可能会引起线程相关问题,一种方法是servlet可以实现SingleThreadModel.。此时,servlet引擎可以(如果必要的话),创建多于一个的实例。如果你惯于使用一般的Singleton servlet,你可能会忘记某些servlet可能会有多个实例。
串行化和重新读取会产生Singleton对象的备份
如果你有一个已经被串行化的对象然后读取两次,你就会获得两个不同的对象,而并不是对同一对象的两个引用。
Java.io包并不是java中仅有的对象串行化技术。现在已经开发了很多使用XML的对象串行化机制,包括SOAP,WDDX等等。在使用这些技术时,你同样要注意Singlton的唯一性问题。
工厂系列模式产生的多个对象问题
在创建性设计模式的factory家族中,通常将工厂设计为一个Singleton。工厂使用于建立其它类实例的Singleton机制。但是如果其它的类没有类似Singleton的保护机制,那么即使工厂本身被正确唯一化,我们也不能保证其它那些类的对象一定能象我们所预期的那样唯一。
Clone造成的多个Singleton对象
上面所有的实现都有一个问题。我们知道,Java允许通过Clone来建立对象。所以如果你要防止Clone,你应该将Singleton定义成如下形式:
final class Singleton {
//…..
}
因为Singleton继承自Object,而Object中的Clone方法被声明为protected.所以客户程序不能直接调用clone,不然编译器就会产生错误。但是如果客户程序从你的Singleton中继承,实现Cloneable接口,并将Clone覆盖声明为public.那么其它程序就可以创建多个Singleton实例对象了。