设计模式之单例模式——应用最广泛的设计模式

单例模式是应用最广的设计模式之一,在应用这个模式的时候,单例对象的类必须保存只有一个实例存在,如果有一个类中包括了很多功能,会消耗很多资源,那么这个类应该采用单例模式,没有理由让它构造多个实例。比如一个类既要访问IO,又要访问数据库等资源,就可以考虑使用单例模式,这种不能自由构造对象的情况,就是单例模式的使用场景。

单例模式的框图大致如下:


其中Client为客户端,Singleton是单例类。

构造单例模式主要有以下几个关键点:

1)构造函数不对外开放。一般定义为private

2)通过一个静态方法(图中的static getIntance()方法)或枚举返回一个单例类对象

3)确保单例类的对象只有一个,尤其是在多线程的环境下,保证线程安全

4)确保单例类对象在反序列化时不会重新构造对象。

通过将单例类的构造函数私有化,使得客户端代码不能通过new的形式手动构造单例类的对象。单例类一般会暴露出一个公有静态方法,客户端需要调用这个静态方法获取到单例类的唯一对象,在获取这个单例类的对象时候需要保证线程安全,即在多线程环境下构造单例类的对象也是有且只有一个,这也是单例模式实现中比较困难的地方。

简单示例:(饿汉模式)

public class Singleton {
	private static Singleton instance;
	//构造方法私有化
	private Singleton() {
		
	}
	//对外开放的静态方法
	public static Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
	
}


可以看出上面就是一个简单的单例模式,被称为饿汉模式,但是没有考虑线程安全,构造方法私有化,对外开放一个静态方法,用于获取实例,如果实例为空,那么久new一个实例返回,如果实例不为空,那么就直接返回instance对象。


下面再贴出考虑线程安全的单例模式代码,首先是懒汉模式的单例模式:

public static synchronized Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
确实这样写可以保证线程安全,但是每次调用getInstance都会进行同步,因为 syncchronized加上在方法上,,这样会消耗不必要的资源,这也是上面的懒汉单例模式最大的问题。




为此又提出了Double Check Lock(DCL)实现单例:


public class Singleton {
	private static Singleton instance;
	//构造方法私有化
	private Singleton() {
		
	}
	//对外开放的静态方法
	
	public static Singleton getInstance() {
		if(instance == null) {
			synchronized (Singleton.class) {
				if(instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
	
}

  可以看出,上面getInstance方法中对instance进行了两次判空,第一层主要是为了避免不必要的同步,第二层的判断则是为了在null的情况下创建实例。为什么要这样呢?为了改善每次调用getInstance方法都要进行同步的问题。但是又带来了新的问题。

现在假设一种情况。

假设线程A执行到instance = new Singleton();语句,这里看起来只有一句代码,但实际上它并不是一个原子操作,这局代码最终会被编译成多条汇编指令,它大致做了3件事情:

1)给Singleton的实例分配内存

2)调用Singleton的构造方法,初始化成员字段

3)将instance对象指向分配的内存空间,此时instance对象就不为null了

但是实际JVM并不是顺序执行以上步骤123的,上面的2和3的顺序无法保证,也就是说有可能是123,也有可能是132,如果是后者,当一个线程A在3执行完毕,2没有执行之前,此时instance已经是非空了,被切换到线程B,这时候instance已经在线程A执行了3,非空,所以当线程B直接取走instance,使用时就会报错,因为instance还有没有执行步骤2.这就是DCL实现的问题。

DCL失效的问题,可以用volatile关键字解决。只需要将instance变量申明为volatile。代码如下:

public class Singleton {
	private static volatile Singleton instance;
	//构造方法私有化
	private Singleton() {
		
	}
	//对外开放的静态方法
	
	public static Singleton getInstance() {
		if(instance == null) {
			synchronized (Singleton.class) {
				if(instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
	
	
}

可以看到将instance对象定义成了volatile。可以保证instance对象每次都是从内存中读取,这样就可以使用DCL的写法来完成单例模式了。但是volatile关键字会影响到一点性能,但是考虑到程序的正确性,牺牲这点性能还是值得的。


那么问题又来了,为什么加了volatile就可以避免DCL失效问题呢?

首先我们知道volatile关键字会影响JVM的性能,volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。 volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。 

关键就在上面标红的地方,volatile屏蔽了JVM的必要的代码优化,所有之前所说的instance = new Singleton();语句所做的三个步骤123就会顺序执行,不会发生132这种情况的执行顺序,因为132这种本身执行顺序就是JVM的一种内部代码优化,既然volatile屏蔽了JVM中必要的代码优化,所以自然不会发生132这种执行顺序。



实际中DCL单例模式使用的最多,但是偶尔会因为java内存模型失败,但是概率是极低的。能够在绝大多数情况下保证单例对象的唯一性。


DCL虽然在一定程度上解决了资源消耗,多余的同步,线程安全等问题,但是某些情况下回失效,虽然概率有点低,加了volatile关键字,又会降低性能。《java并发编程实战》不赞成使用DCL。而建议使用静态内部类单例模式,代码如下。

public class Singleton {
	
	//构造方法私有化
	private Singleton() {
		
	}
	//对外开放的静态方法
	public static Singleton getInstance() {
		return SingletonHoler.instance;
		
	}
	
	private static class SingletonHoler {
		private static final Singleton instance = new Singleton();
	}
}

当第一次加载Singleton类时并不会初始化instance,只有在第一次调用Singleton的getInstance方法时才会导致instance被初始化,也就是说,第一次调用getInstance方法会导致虚拟机加载SingletonHolder类。这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式实现方式。













评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值