并发的单例陷阱
2015-11-22 22:37
316 查看
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; } }
相关文章推荐
- PHP入门(8)-魔术变量
- 释放你的硬盘空间!——Windows 磁盘清理技巧
- R中包的安装方法
- STL——仿函数(函数对象)
- 【总结】大端、小端存储模式的那点事
- libevent的一个bug
- 转帖:PLSQL怎么创建oracle数据库用户
- 扩展欧几里得模板 poj-C Looooops
- iOS setValuesForKeysWithDictionary
- 信息安全系统设计基础第十周学习总结
- 用“计算机”,访问FTP
- 使用Andbase框架实现屏幕适配
- Java super关键字总结
- JAVA环境变量设置
- ssh
- PHP之路
- vs已停止工作的解决方案
- 《Effective Java》笔记
- iOS:自定义模态动画 --UIPresentationController
- Hadoop自定义Writable实现二次排序