Java与模式-单例模式(一)

作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。与单例模式对应的,多例模式中的多例类可以有多个实例,多例类也必须自己创建、管理自己的实例,并向外界提供自己的实例。本文会着重探讨单例模式 ,并把多例模式也介绍一下。单例类看似简单,实则暗藏了很多坑,稍不注意就会出错。

单例模式的特点

单例模式的要点有三个:

(1)单例类只能有一个实例,将构造方法设为private,保证外部无法实例化该类,同时把它设为静态变量;

(2)它必须自行创建这个实例,在类的内部创建,且只创建一次;

(3)它必须自行向整个系统提供这个实例,提供一个静态的工厂方法,返回该类的实例。

从以上的描述中,我们已经可以初见单例模式的端倪。单例模式又分成饿汉式单例模式,懒汉式单例模式,登记式单例模式三种,下面我们一一道来。

1.饿汉式单例类

从单例模式的特点中,我们知道在单例类中定义一个静态域,类型就是这个类本身。饿汉式是一种很形象的叫法,就好像这个类很急迫,不管现在是否需要这个实例,在定义静态变量的定义处就把它初始化,提前初始化。

/**
 * 饿汉式单例模式
 * 在类加载的初始化阶段,instance会被初始化,单从资源利用角度来看,稍差
 * 线程安全的
 * @author cxy
 */
public class EagerSingleton {
	private static final EagerSingleton instance = new EagerSingleton();
	//私有构造方法,保证外界无法直接实例化
	private EagerSingleton() {
		//初始化操作
	}
	//静态工厂方法,线程安全
	public static EagerSingleton getInstance() {
		return instance;
	}
}

饿汉式单例类是线程安全的,但是它在一定程度上造成了浪费,尤其是某些框架里的工厂类,它可能会持有很多类的实例,如果使用饿汉式,那么 会造成加载代价太大。由此,又引出了懒汉式单例类。

2.懒汉式单例类

懒汉式单例类就是采用延迟初始化,延迟到需要域的值时才将它初始化,如果永远不需要这个值,那么这个域就永远不需要初始化。

/**
 * 懒汉式单例模式,使用延迟初始化
 * instance直到使用时才初始化
 * 在多线程环境下,必须同步getInstance()方法
 */
public class LazySingleton {
	private static LazySingleton instance = null;
	//私有的构造方法,保证外界无法直接实例化
	private LazySingleton() {
		System.out.println("只能有一个实例");
	}
	
	/*必须得是synchronized,否则在多线程下会出现多个实例*/
	public synchronized static LazySingleton getInstance() {
		if(instance == null) {
			
			//在非同步的情况下,为了看到多于一个的线程在创建实例
			Thread.yield();
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			instance  = new LazySingleton();
		}
		return instance;
	}
}

一定要注意,getInstance()是一个同步方法,这是为了保证在多线程下,该类仍然只有一个实例。如果不是同步方法会发生什么呢?考虑下面的情况:

(1)线程1调用getInstance(),它首先检查instance是否为空,也就是执行这句话:if(instance == null),一看确实是空的,准备创建吧,恰在这时,它让出了CPU;

(2)线程2也调用getInstance(),它也检查instance是否为空,结果为空,创建实例,现在该类有了第一实例了,完成,返回;

(3)线程1又开始运行了,它该执行这句话了:instance = new LazySingleton();又创建了该类的一个实例,这样它就有两个实例了,这已经不符合要求了。

所以,如果getInstance()必须得同步。但是,创建仅仅只发生了一次,更多的行为是返回这个实例,把getInstance()变成同步方法后,这样每次只能有一个线程调用该方法。为了一个单一行为,却牺牲了常态行为的性能,这样好吗?(注意,如果这个类是不可变的,那么即使getInstance()不同步,它也是安全的)。

双重成例检查

我们分析一下getInstance()方法:其实我们真正要同步的仅仅只是这一句话:instance = new LazySingleton()。明确了这一点后,看下面的代码

if(instance == null) {//第一次检查
	synchronized(LazySingleton.class) {
		if(instance == null) {//第二次检查
			instance = new LazySingleton();
		}
	}
}
这一段理解起来也没有那么难:

(1)说先判断instance是否为空,若为空则去创建该实例;

(2)为了保证只有一个实例,创建语句需要同步,只有获得LazySingleton对应的Class对象的锁之后,才能去执行创建代码;

(3)终于获得了Class对象的锁,要去创建实例了。等一等,刚才可能已经有其他的线程创建了该实例,还是需要判断一下,实例是否为空,若为空,则说明我是第一个进入到同步块的线程,实例还没有创建,那么就让我一个人在毫无打扰的环境下工作吧;若不为空,则说明已经有其他线程创建过实例了,我还是乖乖的享用别人的成果吧。

这样只有第一次创建实例的时候才需要同步,获取实例的时候,由于实例不为空,根本不用进入同步块,这效率,唰唰唰。多么天才的一个想法!接下来,我知道了,这个想法并不是我创造的,之前的人早就使用了,这就是在C语言中大名鼎鼎的双重成例检查(DCL,Double-Checked Locking),如名所示,进行了两次是否为空的检查。可是,双重成例检查在Java中不可行,它已经臭名昭著了,只是他们的解释,着实让人看不懂!

在《Java concurrency in practice》的16.2中,和《Effective Java 2th Edition》第七十一条中,作者提到了真正的原因:在Java 1.5之前的版本中,由于volatile修饰符的语义不够强,双重检查模式的功能很不稳定。他们是JMM规范的制定者,我觉得他们的话确凿无疑的可信。在JMM的后续版本(1.5及以后)中,将instance声明为volatile,那么就能安全的启用DCL了。之所以会出现DCL,是因为JVM在无竞争同步的执行速度很慢,经过优化后,这些都不是问题了,所以使用双重成例检查并不是一种高效的优化措施。

《Effective Java》的第71条:慎用延迟加载。在大多数情况下,正常的初始化要优于延迟初始化。经过衡量之后,必须采用延迟初始化,分两种情况:首先是实例域,采用volatile配合DCL(1.5及以后版本)。

/**
 * 在Java 5.0之后的版本中,使用volatile和DCL可以解决
 * 多线程环境下,实例域的延迟初始化
 */
public class VolatileAndDCL {
	private volatile  VolatileAndDCL instance;
	//私有构造方法
	private VolatileAndDCL() {
		//执行初始化
	}
	/*
	 * 使用双重成例检查,必须配合volatile使用
	 * 只有第一次创建实例的时候需要进入同步块
	 */
	public  VolatileAndDCL getInstance() {
		//使用局部变量result可以提高性能(参考数据:25%),而且更加优雅
		VolatileAndDCL result = instance;
		//第一次检查,只有为空时,才进入同步块
		if(result == null) {
			synchronized(VolatileAndDCL.class) {
				//第二次检查,如果仍为空,创建
				if(result == null)
					instance = result = new VolatileAndDCL();
			}
		}
		return result;
	}
}

对局部变量result的使用让人感到困惑,作者解释到使用局部变量result可以提高性能,而且更加优雅。注意,这里的field是实例域,当然了静态域也可以使用DCL,但是我们有更好的选择:lazy initialization holder class(也称作initialize-on-demand holder class idiom,延迟初始化站位类模式),这种模式使用一个静态内部类持有外围类静态域(也就是instance):

class StaticFieldSingleton {
	/**
	 * 使用静态内部类来延迟初始化instance,
	 * 在加载StaticFieldSingleton时,不会对FieldHolder初始化
	 * 直到读取field时才对FieldHolder初始化,也就是对instance初始化
	 */
	private static class FieldHolder {
		static final StaticFieldSingleton instance = new StaticFieldSingleton();
	}
	
	private StaticFieldSingleton() {
		//执行一些初始化操作
	}
	//完全不需要同步
	public static StaticFieldSingleton getInstance() {
		return FieldHolder.instance;
	}
}

当加载StaticFieldSingleton类时,并不会对初始化FieldHolder类,只有第一次读取getInstance()时,FieldHolder类才会初始化。这种模式的魅力在于:首先它保证了延迟初始化的优点,同时getInstance()又没有被同步,也就没有增加任何访问开销。现代的VM在初始化FieldHolder类时,会同步对intance的访问,保证多线程下的初始化过程中的安全性。一旦这个类被初始化,VM将修补代码,以便后续对instance的访问不会导致任何测试或同步。这个思路,简直就简直了!

通过对懒汉式单例模式的分析,我们大致可以得出如下结论:慎用延迟初始化,必须经过测量类在用和不用延迟初始化时的性能差别之后,再做决定。如果是实例域的延迟初始化,那么就是用volatile配合DCL;如果是静态域的延迟初始化,使用lazy initialization holder class模式。

3.登记式单例模式

在单例模式中,要求构造方法是private,这样外部无法直接实例化该类的对象,但是构造方法是private,也就才造成了该类不可被继承的缺点。为了克服这个缺点,GoField又提出了登记式单例模式,在父类中使用一个map来持有所有子类的实例,也就是说:说有子类到父类的map中登记,当要获得该类实例时,去Map中获得该类的实例。

/**
 * 登记式单例类
 * 是为了克服饿汉式和懒汉式均不能继承的缺点而设计的
 * 子类的实例化只能是懒汉式
 * @author cxy
 */
public class RegSingleton {
	//所有子类都要到registry中去登记
	private static HashMap<String,RegSingleton> registry = new HashMap<String, RegSingleton>();
	//饿汉式实例化 
	static {
		RegSingleton instance = new RegSingleton();
		registry.put(instance.getClass().getName(), instance);
	}
	
	//为了让子类可以继承,不能为私有
	protected RegSingleton() {	}
	
	/**
	 * 静态工厂方法,返回指定类的唯一实例。
	 * 首先去registry中查找是否有该类的实例,如有,直接返回。
	 * 如果没有,使用反射机制生成该类的实例,并放到registry中。
	 */
	public static RegSingleton getInstance(String name) {
		//如果为空,则返回此类的实例
		if(name == null) {
			name = "com.javapatterns.singleton.RegSingleton";
		}
		/**
		 * 如果返回的是null,说明这个子类没有登记过
		 * 则创建子类的实例,并且登记到registry中
		 */
		if(registry.get(name) == null) {
			try {
				//在Java中,由于反射的存在,可以让子类的构造方法是私有的,就像下面注释的那样实现
				//但这已经是语言的特性了,超出了设计模式的范畴了
				//RegSingleton t_instance =  (RegSingleton) Class.forName(name).getDeclaredConstructors()[0].newInstance();
				registry.put(name, (RegSingleton) Class.forName(name).newInstance());
			}catch(Exception e) {
				System.out.println("实例化对象失败");
			}
		}
		return registry.get(name);
		
	}
}

/**
 * 子类需要父类的帮助才能实例化
 */
 class RegSingletonChild extends RegSingleton {
	public RegSingletonChild(){}
	
	/**
	 * 静态工厂方法
	 */
	public static RegSingletonChild getInstance() {
		return (RegSingletonChild)RegSingleton.getInstance("com.javapattens.singleton.RegSingletonChild");
	}
}

通过上面的代码,我们可以看出父类的构造方法不再是private的了,这是登记式单例模式的一个缺点。这个登记式单例模式是根据Java语言的特点来写的,还有其他的形式。

使用单例模式的条件

使用单例模式有一个必要条件:在一个系统要求一个类只有一个实例时才应当应用单例模式。反过来说,如果一个类可以有几个实例共存,那么就没有必要使用单例类。

模式学习总是简单,但是如何使用,在什么情况下,却是一个很需经验和技巧的事情。且学且珍惜吧!

上面介绍了单例模式的特点,三种类型的单例模式,以及各自的优缺点及注意事项,着重谈论了懒汉式单例模式的延迟初始化问题,这一部分已经超出了设计模式的范畴了,但是,我觉得仍然很有必须花时间去弄明白。一般情况下,饿汉式单例模式已经够使用了,如果需要延迟加载,本文也已经给出了完美的解决方案。由于单例模式内容很多,因此分成两篇来探讨,下一篇讲一下多例模式,以及单例模式和多例模式的一个实际应用。

----------------------------------------------------------------- 更新--------------------------------------------------------------------------------------

单元素的枚举

Java 1.5中提供了枚举类型,枚举类型是实现单例类的最佳方法,而且更加简洁。由于枚举类实现了Serializable接口,使用枚举实现的单例类可以直接享受枚举提供了的序列化机制,绝对多次实例化。使用枚举出了可以实现单例,还可以实现有限例,其情形与单例相似,给出示意性代码:

public enum Singleton {
	Instance(some args);
	
	private Singleton(some args) {
		//初始化
	}
	
	//提供的方法
	public void f() { }
	public XXX xxx(xxx) { }
}

参考资料:《Java与模式》,《Effective Java 2th Edition》,《Java并发编程实战》,GoF《设计模式》。


你可能感兴趣的:(java)