单例模式

单例模式介绍:
通常在一个应用中,需要有一些全局对象,这样有利于协调系统整体的行为。例如一个应用中,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版本是否过低、单例对象的
资源消耗等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值