通常在一个应用中,需要有一些全局对象,这样有利于协调系统整体的行为。例如一个应用中,Android开发中都建议使用application级别的Context,这个时候就需要将这个Context做成单例对象,没有必要每次调用的时候都实例化一次。一般这就是单例模式的使用场景。
实现方式:
懒汉式模式:
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}
优点:单例只有在使用的时候才会被实例化。
缺点:第一次加载的时候需要及时进行实例化,反应稍慢,每次调用getInstance都要进行同步,造成不必要的同步开销。不建议使用。
Double Check Lock实现单例:
public class Singleton{
private volatile static Singleton sInstance = null;
private Singleton(){}
public void doSomething(){
System.out.println("doSomething");
}
// 只在第一次初始化的时候加上同步锁
public static Singleton getInstance(){
if(sInstance == null){
synchronized(Singleton.class){
if(sInstance == null){
sInstance = new Singleton();
}
}
}
}
return sInstance;
}
本程序的最大亮点就是getInstance方法上,保证只有在第一次初始化的时候才会同步。这种方法貌似很完美的解决了上述效率的问题,它或许在并发量不多,安全性不太高的情况能完美运行,但是,这种方法也有不幸的地方。
问题就是出现在这句 sInstance = new Singleton();
分析如下:
假如现在线程A执行到到了sInstance = new Singleton()这句话,看似只是一句简单的初始化语句,实际上它并不是一个原子操作(原子操作的意思是要么执行完,要么就不执行,不存在执行一半的这种情况),这句代码最终会被编译成多条汇编指令,它大致做了3件事:
(1)给Singleton的实例分配内存
(2)调用Singleton()的构造函数,初始化成员字段
(3)将sInstance对象指向分配的内存(sInstance不为null)
但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,
上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,
被切换到线程B上,这时候sInstance因为已经在线程A内执行过了第三点,instance已经是非空了,所以线程B直接拿走sInstance,然后使用,然后报错。 DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static Singleton sInstance = null;”
就可以保证每次都去instance都从主内存读取,就可以使用DCL的写法来完成单例模式。当然volatile或多或少也会影响到性能,最重要的是我们
还要考虑JDK1.42以及之前的版本.
优点:资源利用率高,第一次执行getsInstance时单例对象才会被实例化,效率高。
缺点:第一次加载时反应稍慢,也由于Java内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷。DCL是使用最多的单例模式,并且能够在
绝大多数场景下保证单例对象的唯一性,除非你的代码在并发场景比较复杂或低于JDK1.6版本下使用,否则这种方式一般可以满足需求。
静态内部类单例模式:
public class Singleton{
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
//静态内部类
private static class SingletonHolder{
private static final Singleton sInstance = new Singleton();
}
}
此举是对DCL的优化,优点是当第一次加载Singleton类时并不会初始化sInstance,只有在第一次调用Singleton的getInstance方法时
才会导致sInstance被初始化。因此第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能保证线程安全,
也可以保证单例对象的唯一性。同时也延迟了单例对象的实例化。
枚举单例:
public enum SingletonEnum{
INSTANCE;
public void doSomething(){
System.out.println("doSomething");
}
}
写法简单是枚举最大的优点,最重要的是默认的枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。
在上述的几个单例模式实现中,在一个情况下它们会出现重新创建对象的情况,那就是反序列化,类实现了Serializable接口
原本通过序列化可以将一个单独的实例对象写到磁盘,然后再读回来,获得一个单例。即使构造方法是私有的,反序列化依然可以通过特殊途径去创建类的一个新的实例,相当于调用类的构造函数。为了解决这一问题,我们需要使用反序列化操作提供的一个函数,类中具有一个私有的、被实例化的方法readSolve(),这个方法可以让开发人员控制对象的反序列化。如下:
private Object readResolve() throws ObjectStreamException{
return sInstance;
}
也就是在readResolve()的时候返回单例对象,而不是默认的重新生成一个新的对象,对于枚举来说,并不存在这个问题。使用容器实现单例模式:
public class SingletonManager{
private static Map<String,Object> objMap = new HashMap<String,Object>();
private SingletonManager(){}
public static void registerService(String key,Object instance){
if(!objMap.containsKey(key)){
objMap.put(key,instance);
}
}
public static Object getService(String key){
return objMap.get(key);
}
}
程序的一开始,将多种单例类型注入到一个单例管理类中,使用的时候需要使用getService()方法根据key获取对象对应类型的对象。这种方式可以便于关联很多单例,使用统一的接口进行获取。
单例的核心:将构造函数私有化,并且通过静态方法获取一个唯一的实例,获取过程中需要保证线程安全、防止反序列化导致
重新生成实例对象等问题。选择哪种方式实现需要考虑到是否是复杂的并发环境、JDK版本是否过低、单例对象的
资源消耗等。