并发的单例陷阱

1  并发的单例是什么样的

1.1 单机的singleton 1.0

在编程的时候,我们会用到单例模式,尤其是一些工具类或数据库连接类常常是单例的。因为他们经常被使用,重复的初始化成本又比较高,因此写成单例模式的。我们都知道单例只有一个对象存在,通常会写成下面这种形式。

public class SingletonCommon
{
	private static SingletonCommon instance;
	private SingletonCommon()
	{
		
	}
	
	public static SingletonCommon getInstance()
	{
		if(null==instance)    //1
		{
			instance=new SingletonCommon();//2
		}
		return instance;
	}

}

这很容易想到,相信接触过单例设计模式的人都能写出来。但是如果在并发场景里呢?如果线程A正在执行代码2初始化对象但为完成,而线程B执行到代码1返回false,可能会看到一个还未初始化完成的对象。这显然不是我们想要的,于是就有了下面的代码

1.2 并发的 singleton 2.0

为了并发的需要,我们容易想到的就是同步。

public class SingletonSynchronized {
	private static SingletonSynchronized instance;
	private SingletonSynchronized()
	{
		
	}
	public synchronized static SingletonSynchronized getInstance()
	{
		if(null==instance)
		{
			instance=new SingletonSynchronized();
		}
		return instance;
	}

}

这样显然是没有问题的。但是synchonized将导致性能开销。如果getInstance()方法被频繁调用,程序的性能会比较差。但是如果不那么频繁的话,这样写还是很不错的。那能不能再改进一下呢?

1.3 并发的 singleton 2.0S

相信肯定有人用过双重检查锁的方式,在Spring的源码中也有用到。不得不说第一次见到这种方式的时候彻底膜拜了,先来看看我们的单例如何实现。

public class SingletonSynchronizedSuper {
	private static SingletonSynchronizedSuper instance;
	private SingletonSynchronizedSuper()
	{
		
	}
	public static SingletonSynchronizedSuper getInstance()
	{
		if(null==instance)//第一次检查
		{
			synchronized (SingletonSynchronizedSuper.class) //类加锁
			{
				if(null==instance) //第二次检查
				{
					instance=new SingletonSynchronizedSuper();//但是这里有问题!!!
				}
			
			}
			
		}
		return instance;
	}

}

在这段代码中,第一次检查如果不为null,则不必加锁。因此看起来似乎减少了synchronized的范围,在多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。在对象创建好之后,执行getInstance()将不需要获取锁,直接返回已创建好的对象。然而不要高兴的太早,就是在第一次检查的时候,如果instance不为null,得到的依然有可能是未初始化完成的对象。


2 陷阱在这里

2.1 指令重排序

下面就来解释为什么上面的写法是有问题的。Java创建(new)一个对象的时候大致分为以下三步

(1) 分配对象空间

(2) 初始化对象

(3)返回instance的地址

他们对应着JVM里面的三条指令,我们理所当然地认为这三个指令就是这个顺序执行。但是编译器会对指令进行优化,他会重排序执行,只要不影响程序执行结果。事实上如果把2和3的顺序调整一下是不是可以提高程序性能呢?这有点像同步执行和异步执行。事实上在一些编译器里面确实这样做了,那么new的时候执行的顺序就是13 2了。

2.2 问题的根源

上面的顺序执行完,再回去看看代码,是不是能想到并发的情况下如何出错了。A线程初始化对象的过程中,执行到3号执行返回instance地址,此时instance不为null,然后执行指令2初始化对象。而B线程发现instance不为null的时候直接返回,这时候A还没有初始化完成对象,也就得到了未完成初始化的对象。这样就出问题了。

当然待A初始化完成以后,B得到的instance也初始化完成了,这时也就没什么问题了。但是多线程的情况谁能说得准呢?他又不会那么听话。下面就来看看如何解决吧。

3 跳出陷阱

3.1 volatile

这个方法很简单,为instance增加一个volatile关键字。这种弱锁可以有效地解决指令重排序的问题,而且性能上也要比sychornized好一些。

public class SingletonSynchronizedSuper {
	private  volatile static SingletonSynchronizedSuper instance;
	private SingletonSynchronizedSuper()
	{
		
	}
	public static SingletonSynchronizedSuper getInstance()
	{
		if(null==instance)//第一次检查
		{
			instance=new SingletonSynchronizedSuper();
		}
		return instance;
	}

}






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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值