单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。
单例的特点
-
单例类只能有一个实例。
-
单例类必须自己创建自己的唯一实例。
-
单例类必须给所有其他对象提供这一实例。
单例模式的7种写法
第一种(懒汉,线程不安全):
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式采用了懒加载模式,在第一次使用时才去初始化对象,节省了资源,但是第一次使用时需要初始化,反映稍微慢一些,同时会产生线程安全的问题。 这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。
第二种(懒汉,线程安全):
public class Singleton {
private static Singleton singleton ;
public static Singleton getInstance(){
synchronized (Singleton.class) {
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
或者
public class Singleton {
private static Singleton singleton ;
public synchronized static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
这种方式可以在多线程环境下安全工作,但是每次调用getInstance()方法都要进行同步,效率太低,造成不必要的开开销,而且大部分时候我们是用不到同步的,所以不建议采用这种方式。
第三种(饿汉):
public class Singleton{
private static Singleton instance = new Singleton();
private Singleton () { }
public static Singleton getInstance() {
return instance;
}
}
这种方式基于classloader机制,避免了多线程的同步问题,不过instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法,但也不确定有其他的方式或者说静态方法导致类装载,此时初始化instance显然没有达到lazy loading的效果。
反射是可以破坏单例属性的。因为我们通过反射把它的构造函数设成可访问的,然后去生成一个新的对象。
改进版:
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {
System.err.println("Elvis Constructor is invoked!");
if (INSTANCE != null) {
System.err.println("实例已存在,无法初始化!");
throw new UnsupportedOperationException("实例已存在,无法初始化!");
}
}
}
注意:通过对序列化后的Elvis 进行反序列化得到的对象是一个新的对象,这就破坏了Elvis 的单例性。
参考:http://www.importnew.com/29343.html
第四种(饿汉,变种):
pubic class Singleton {
private Singleton instance = null;
static {
instance = new Singleton;
}
private Singleton () {};
public static Singleton getInstance() {
return this.instance;
}
}
第三种方式差不多,都是在类初始化即实例化instance。
第五种(静态内部类):
public class Singleton {
private static class SingletonHolder{
private static final Singleton INSTSNCE = new Singleton();
}
private Singleton () {}
public static final Singleton getInstance () {
return SingletonHolder.INSTANCE;
}
}
首先需要明确一个结论:加载一个类时,其内部的类不会同时被加载,当且仅当某个静态成员(静态成员变量,构造方法,静态方法)被调用时才去加载。第一次加载Singleton时并不会初始化singleton,只有第一次掉用那个getInstance()方法时才会加载SingletonHolder,并且初始化singleton,这样不仅能够保证线程的安全性也能保证Singleton类的唯一性,所以推荐使用这种方式。
这种方式同样利用了classloader的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式细微的差别是前两种是只要Singleton类被装载了,那么instance就会被实例化,也就没有达到lazy loading效果,而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingleHolder类没有被主动使用,只有显示通过调用getInstance方法时才会显示装载SingleHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。
第六种(枚举):
public class Singleton {
INSTANCE;
public void whateverMethod() {
}
}
这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。
你可以通过Singleton.INSTANCE来访问,比较方便,线程安全,防止反序列化创建新的对象,但是失去了一些类的特性,没有延迟加载,而且可读性较差,所以很少有人使用。
第七种(双重校验锁):
public class Singleton {
private volatile static Singleton singleton;
private Singleton () {}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这种写法在getInstance()方法中进行了两次空判断,第一次是为了避免不必要的同步,第二次是在singleton为空的情况下才创建实例。DCL虽然在一定程度上解决了资源的消耗和多余的同步,线程安全等问题,但是在某些情况下会出现DCL失效。在某些书中建议使用静态内部类单例模式来代替DCL. 在JDK1.5之后,双重检查锁定才能够正常达到单例效果
第七种(容器式):
public class SingletonManager {
public static Map<String, Object> objMap = new HashMap<String, Object>();
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);
}
}
用SingletonManager将多个单例统一进行管理,使用时根据key获取对应的实例,这种方式可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,屏蔽了内部的实现细节,降低了耦合度。
总结
有两个问题需要注意:
1.如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
2.如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
对第一个问题修复的办法是:
private static Class getClass(String classname) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
}
对第二个问题修复的办法是:
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {
}
private Object readResolve() {
return INSTANCE;
}
}
第三种和第五种方式,简单易懂,而且在JVM层实现了线程安全(如果不是多个类加载器环境),一般的情况下,我会使用第三种方式,只有在要明确实现lazy loading效果时才会使用第五种方式,另外,如果涉及到反序列化创建对象时我会试着使用枚举的方式来实现单例,不过,我一直会保证我的程序是线程安全的,而且我永远不会使用第一种和第二种方式,如果有其他特殊的需求,我可能会使用第七种方式,毕竟,JDK1.5已经没有双重检查锁定的问题了。
不过一般来说,第一种不算单例,第四种和第三种就是一种,如果算的话,第五种也可以分开写了。所以说,一般单例都是五种写法。懒汉,恶汉,双重校验锁,枚举和静态内部类。