今天去jdon,看了它的设计研究栏目,bang有几篇评论单例模式的文章,声称“Singleton is evil”(见http://www.jdon.com/jive/article.jsp?forum=91&thread=17578),并且引用几篇外文页面佐证自己的观点,其中有一篇文章更是说,单例不仅不是一种模式,而是一种反模式。
下面我谈谈我对单例模式的看法。逐一分析单例模式的陷阱,帮助大家正确使用单例模式。
(1) 陷阱一:调用函数的性能瓶颈
在c++中,单例只有一种实现方式——LazySingleton, 实现如下(本文全部使用java代码):
public class LazySingleton {
private static LazySingleton m_instance = null ;
private LazySingleton(){};
synchronized public static LazySingleton getInstance(){
if(m_instance==null)
m_instance=new LazySingleton();
return m_instance;
}
}
LazySingleton将对象的初始化推迟到调用的时候。并且为了防止多线程环境下产生多个实例,使用synchronized关键字保证函数getInstance调用的线程安全。synchronized关键字的存在保证了只会产生一个对象,但也成了多线程环境下的性能瓶颈。一个多线程的程序,到了这里却要排队等候成了一个单线程式的执行流程,这在高并发环境下是不可容忍的。而c++中可以使用双重检查机制将这种性能问题仅仅限制在第一次构造对象的时候,而java中不可以使用双重检查机制。
但是java可以实现EagerSingleton,实现如下:
public class EagerSingleton {
private static EagerSingleton m_instance = new EagerSingleton();
private EagerSingleton(){};
public static agerSingleton getInstance(){
return m_instance;
}
} 与LazySingleton相比,EagerSingleton将对象的初始化放到了类加载的时候。这样就避免了synchronized关键字的性能瓶颈。
(2)陷阱二:访问互斥共享资源
EagerSingleton中访问互斥资源也要考虑线程安全问题。下面看一个例子:
public class EagerSingleton{
private static EagerSingleton m_instance=new EagerSingleton();
private HashMap map=new HashMap();
private EagerSingleton(){};
public static agerSingleton getInstance(){
return m_instance;
}
public void refreshMap(Object key){
synchronized(map){
if(!map.contains(key))
map.put(key,value);//value为此时的实时数据
}
}
}因为该类是单例,可能多线程并发访问map,map非线程安全,需要加线程安全关键字,否则就掉入了访问互斥资源的陷阱。
(3)陷阱三:非法逻辑陷阱
这种情况一般是滥用单例模式造成的,下面考虑一种滥用单例的情况。下面的代码的作用是getValueByName后,马上printValue即完成操作流程。
public class EagerSingleton{
private static EagerSingleton m_instance=new EagerSingleton();
private String value=null;
private EagerSingleton(){};
public static agerSingleton getInstance(){
return m_instance;
}
synchronized public void getValueByName(String name){
value=getByNameFromDateBase(name);
}
public viod printValue(){
System.out.println(this.vaue);
}
}
该类含有一私有属性value,在多线程环境下不能保证value值的合理逻辑,一线程getValueByName后,马上printValue,也有可能value的值已经被其他线程修改。这种情况就属于单例模式的滥用,该类根本不适合做成单例。
消除非法逻辑的陷阱,可以通过将该类重构为纯粹的行为类完成。重构后的代码如下:
public class EagerSingleton{
private static EagerSingleton m_instance=new EagerSingleton();
private EagerSingleton(){};
public static agerSingleton getInstance(){
return m_instance;
}
private String getValueByName(String name){
return getByNameFromDateBase(name);
}
public viod printName(String name){
String value=getValueByName(String name);
System.out.println(value);
}
}
通过调用printName(String name)直接完成操作流程,将其中的私有属性处理成过程式的参数传递,将该类修改成纯粹的行为类。
含有私有属性并且含有对它赋值操作的类并非都会调入该陷阱,构造函数里进行对私有属性赋值不会引起非法逻辑,如下代码
public class EagerSingleton{
private static EagerSingleton m_instance=new EagerSingleton();
private HashMap map==new HashMap();
private EagerSingleton(){
map.put(key,value);//value为此时的实时数据
}
public static agerSingleton getInstance(){
return m_instance;
}
}
构造函数里不必要加线程安全关键字也可以保证线程安全,因为类加载器是线程安全的,EagerSingleton只会在类加载的时候实例化一次,这样不会出现单例模式的线程不安全,也不会造成非法逻辑。
(4)陷阱四:单例陷阱的传递
当含有对象作为单例类的私有属性时,陷阱不仅会出现在该类本身,还会传递到私有对象所在的类中。看如下代码:
public class EagerSingleton{
private static EagerSingleton m_instance=new EagerSingleton();
private NewClass newClass=nll;
private EagerSingleton(){
newClass=new NewClass();
};
public static agerSingleton getInstance(){
return m_instance;
}
public viod printName(String name){
String value=newClass.operationByNameAndReturnValue(String name);
System.out.println(value);
}
}乍一看,代码中除了构造函数对私有属性进行了初始化操作,其他地方没有对私有属性的赋值,不会引起非法逻辑陷阱。其实这个赋值操作可能隐含在newClass.operationByNameAndReturnValue(String name)操作,只有保证了NewClass的operationByNameAndReturnValue操作不会对它的私有属性赋值操作,才能保证真正的合理逻辑。同样,只有保证NewClass的operationByNameAndReturnValue操作没有掉入访问互斥资源陷阱,才能真正保证EagerSingleton没有掉入该陷阱。
消除该陷阱的方法:(1)类方法的名称要合理,比如纯粹的行为方法名:interprete,excute,operation之类的方法中就不该含有对私有属性直接或者间接的赋值操作,每个方法的责任要明确。(2)单例类中尽量不要含有非单例类的实例作为私有属性(容器类除外),一定要有类的实例作为私有属性的时候,重新审视这个作为私有属性的类,是不是也应该设计成单例类;或者保证对它的初始化赋值限制在构造函数内。