Singleton

今天学习的这个模式比较简单,就是常说的单例模式。其实在看《Head First 设计模式》这本书之前我就会用单利模式,但是不能没有深入理解单利模式。可能在看到这篇文章的时候你也会单利模式,但是我下面讲到的知识点你也可能不知道哟。话不多说先来一段单例模式的代码


1 下面这段代码是我常写的单例模式。我说说下面这段代码的有点和缺点吧。

优点:只有在用到单实例的时候才会新建对象。换句话说,如果你的程序中可能用不到这个单实例对象,那么你的程序就不会创建这个对象,如果这个单实例对象是很耗费资源的对象,那么这样的单实例是很节约资源的。

缺点:在多线程中,下面的单实例可能会创建出多个对象,假如 某时刻singleton为null 这时有两个线程分别为 线程1 和线程2 ,线程1进入getInstance()方法,判断singleton为null,线程一准备创建singleton对象,单还没有创建。此时线程二进入getInstance()方法判断singleton对象也为空,也准备创建对象(但还没有创建)。此时线程1又得到资源,创建好singleton(假设该singleton为singleton1)后返回对象singleton1。然后线程2得到资源继续创建对象singleton(假设该singleton为singleton2)后返回对象singleton2.那么系统将会得到两个对象。你设计的是单实例,只希望得到一个对象,但是系统可能会产生两个对象。这可能给系统带来灾难性后果。

public class Singleton {

	private static Singleton singleton;

	private Singleton(){

	}

	public static Singleton getInstance(){
		if(null == singleton){
			singleton = new Singleton();
		}

		return singleton;
	}
}


2 为了解决上面单例模式带来的问题,我们提出了另外一种单实例的写法。但是这种写法与第一种方法优缺点是互逆的。程序一编译就会产生一个单实例对象。万一这个对象很费资源并且我们系统可能有用不到者个对象呢?,不过下面这种写法确实是解决了多线程出现创建多个线程的问题。因为在静态初始化器中创建单例,确保了线程安全。系统只会创建一个单实例对象。


public class Singleton {

	private static Singleton singleton = new Singleton();

	private Singleton(){

	}

	public static Singleton getInstance(){
		return singleton;
	}
}



3 既然上面两种方式都有缺点,有没有稍微完美点的单实例的创建方法呢?其实在仔细理解第一个单实例的时候我就想到了,既然多线程导致了问题,那我就用一个同步嘛。下面来看看具体的代码实现。在getInstance方法中,有两个非空判断,第一个非空判断,确保在singleton为null的时候,才进行同步操作,避免了每次调用getInstance的时候,都进行同步操作(要知道同步操作是比较费性能的)。第二个非空判断是为了再次确定singleton为null的时候才创建单实例,确保只会产生一个单实例。


public class Singleton {

	private volatile static Singleton singleton;

	private Singleton(){}

	public static Singleton getInstance(){
		if(singleton == null){
			synchronized(Singleton.class){
				if(singleton == null){
					singleton = new Singleton();
				}
			}
		}

		return singleton;
	}
}

需要说明的是 singleton必须要使用volatile 修饰.原因如下:

在 singleton = new Singleton();创建对象过程,实例化一个对象要分为三个步骤

  1. 分配内存空间

  2. 初始化对象

  3. 将内存空间的地址赋值给对应的引用

如果不使用volatile修饰singleton,那么由于编译器执行的指令重排序2、3的顺序可能发生变化,即先3 后2 那么存在这种情况:

线程A 调用getinstance方法执行singleton  = new singleton ()的时候,参考上面的创建对象的过程,可能存在这种情况singleton != null (因为执行了3 ),但是由于2还未执行,singleton还未实例化完成,即是一个不完整的对象。

此时线程B访问getinstance()方法,并返回了这个不完整的singleton,那么在调用singleton时就可能会出错(因为其还未初始化完全)

如果使用volatile修饰singleton则告诉编译器不要对 singleton的创建过程做指令重排序的操作,既不会出现 singleton != null并且singleton还未初始化完全的情况。因此volatile修饰的singleton创建出来是安全的。


4 虽然第三条看上去已经解决了所有的问题,但是,JSR133并不推荐我们使用这样的单例,详见

Does the new memory model fix the "double-checked locking" problem?

在这里JSR133推荐了一种更好的单例模式的写法

如果有同学觉得下面的代码与 2 中的方式有相同之处,请参看周志明写的《深入理解java虚拟机》的第七章,了解虚拟机类加载机制就会明白下面代码与2中代码的区别。

public class Singleton {

	private Singleton(){}

	private static class SingletonHolder{
		static Singleton SINGLETONHOLDER = new Singleton();
	}

	public static Singleton getInstance(){
		return SingletonHolder.SINGLETONHOLDER;
	}
}

JSR133给出的理由是:

在新的内存模型下,使用volatile的性能成本上升,几乎达到同步成本的水平。因此,仍然没有充分的理由使用双重检查锁定(DCL)来写单实例


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值