下面我谈谈我对单例模式的看法。逐一分析单例模式的陷阱,帮助大家正确使用单例模式。
(1) 陷阱一:调用函数的性能瓶颈
在c++中,单例只有一种实现方式——LazySingleton, 实现如下(本文全部使用java代码):
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,实现如下:
private static EagerSingleton m_instance = new EagerSingleton();
private EagerSingleton(){};
public static agerSingleton getInstance(){
return m_instance;
}
}
(2)陷阱二:访问互斥共享资源
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为此时的实时数据
}
}
}
(3)陷阱三:非法逻辑陷阱
这种情况一般是滥用单例模式造成的,下面考虑一种滥用单例的情况。下面的代码的作用是getValueByName后,马上printValue即完成操作流程。
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的值已经被其他线程修改。这种情况就属于单例模式的滥用,该类根本不适合做成单例。
消除非法逻辑的陷阱,可以通过将该类重构为纯粹的行为类完成。重构后的代码如下:
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)直接完成操作流程,将其中的私有属性处理成过程式的参数传递,将该类修改成纯粹的行为类。
含有私有属性并且含有对它赋值操作的类并非都会调入该陷阱,构造函数里进行对私有属性赋值不会引起非法逻辑,如下代码
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)陷阱四:单例陷阱的传递
当含有对象作为单例类的私有属性时,陷阱不仅会出现在该类本身,还会传递到私有对象所在的类中。看如下代码:
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);
}
}
消除该陷阱的方法:(1)类方法的名称要合理,比如纯粹的行为方法名:interprete,excute,operation之类的方法中就不该含有对 私有属性直接或者间接的赋值操作,每个方法的责任要明确。(2)单例类中尽量不要含有非单例类的实例作为私有属性(容器类除外),一定要有类的实例作为私 有属性的时候,重新审视这个作为私有属性的类,是不是也应该设计成单例类;或者保证对它的初始化赋值限制在构造函数内。