Topic: 浅析Singleton模式 |
Print this page |
1.浅析Singleton模式 | Copy to clipboard |
Posted by: fat32 Posted on: 2003-05-09 15:13 一、意图 在很多情况下我们要求一个类只有一个实例,例如数据库连接缓冲池类。我们需要一种机制来保证使用该类的程序能够比较方便的得到这个类的单一实例,并且要禁止创建该类的第二个实例。解决这一问题的方法是让类自身保存它的唯一实例,这个类必须保证其他程序不能创建该类的其他实例,并且要提供访问这个唯一实例的方法。这就是Singleton模式。 二、Singleton模式的Java实现 我们首先给出Singleton模式的Java实现样本: Listing 1 :
首先这个类将自己的构造方法声明为private,这使得使用该类的程序无法创建这个类的实例。然后通过声明一个静态属性instance来保存该类的唯一实例并且提供了访问该单一实例的方法getInstance()。 很多资料上给出了类似如下的实现样本: Listing 2 :
这种实现方式采用了lazy Initialization方式,保证只有在第一次使用该类的时候才创建该类的实例,似乎比Listing1中的方式有所改进。但请注意,当引入多线程时我们就会发现这段代码并不能保证只有一个Singleton实例被创建!我们考虑有两个线程同时调用getInstance()方法的情况下,假如发生下面的事件序列: 1) 线程1调用getInstance()方法并在//1检测到instance为null 2) 线程1进入到if代码区,但在执行//2之前被线程2抢占 3) 线程2调用getInstance()方法并在//1检测到instance为null 4) 线程2进入到if代码区,创建一个Singleton对象,并把该对象的句柄付给instance变量 5) 线程2在//3将instance句柄返回 6) 线程2被线程1抢占 7) 线程1从被抢占的地方开始继续执行//2,从而创建了第二个Singleton对象 8) 线程1在//3返回该对象 结果Listing2的实现方式造成了多个Singleton对象创建,违反了Singleton设计模式的初衷。因此在多线程的情况下,必须引入同步机制。我们看下面的代码: Listing3:
这段代码是很严谨的,通过synchronized关键字的使用,保证了在同一时刻只能有一个线程进入getInstance()方法。从而避免了Listing2出现的问题。然而,我们只要仔细研究一下就会发现这段代码也并非完美,因为synchronized只有在getInstance()方法第一次被调用时才有必要,而此后的所有调用都没有必要同步了,因为Singleton对象已经是non-null值,真正需要同步的//2代码再也不会被执行到。然而因为整个方法被同步了,每次调用该方法你都要付出不必要的同步代价。早期的JVM实现,同步代价是很高的,虽然新版本的JVM已经有很大改善,但作为一个优秀的程序员,你总要想让自己的代码完美,没有人愿意付出不必要的代价,因为这实在是一件很不爽的事。那么,我们再看下面的更改: Listing4:
我们来看下面的事件序列: 1) 线程1进入getInstance()方法 2) 因为instance为null,线程1进入//1的synchronized代码区 3) 线程1被线程2抢占 4) 线程2进入到getInstance()方法 5) 因为instance仍然为null,线程2试图获取//1处的同步锁,然而因为该同步锁被线程1占有,线程2在//1处被阻塞。 6) 线程2被线程1抢占 7) 线程1继续执行,因为instance在//2处仍然为null,线程1创建一个Singleton对象并将句柄付给instance变量 8) 线程1退出synchronized代码区并将instance对象返回 9) 线程1被线程2抢占 10) 线程2获得//1处的同步锁并在//2检查instance是否为null 11) 因为instance已经不再是null,线程2不能执行//3的代码,也就不能创建第二个Singleton对象了。以后所有对getInstance()方法的调用都将直接返回此instance,不会再进入synchronized代码区。 Listing4中所采用的方法就是Java程序设计里经常被提到的“双层检查锁(double-checked locking)”。这个双层检查锁的理论看起来非常完美,但不幸的是实际和理论总是不一样,这样的代码仍然不能保证正常运行。为什么呢?这和当前Java平台的内存模型(Memory Model)有关。当前的内存模型允许一种被称为“乱顺序写(out-of-order writes)”的操作,这是造成“双层检查锁”失败的主要原因。 我们来看Listing4中的//3代码行,这行代码创建一个新的Singleton对象实例并使instance变量指向这个对象。问题就出在instance变量可能会在Singleton的构造函数执行之前变成non-null值。你可能认为这不可能,但实际上这是完全可能的,因为有一些JIT编译器的对instance=new Singleton()的实现类似如下的伪码: Listing5:
上面这段伪码首先将分配的内存付给instance变量,然后再调用Singleton的构造方法,使的instance变量在得到完整的Singleton对象之前变为non-null值,这正是问题所在。再回到Listing4,我们来看下面的事件序列: 1) 线程1进入getInstance()方法 2) 因为instance为null,线程1进入到synchronized代码区 3) 线程1执行到//3,根据Listing5伪码的实现方式将instance变量置为non-null值,但在调用Singleton构造方法之前被线程2抢占 4) 线程2检查instance的值,发现为non-null值,并将这个不完整的Singleton对象返回 5) 线程2被线程1抢占 6) 线程1继续执行Singleton类的构造方法,并将对象实例返回 这个事件序列造成线程2返回了一个不完整的Singleton对象,必定引起程序异常。并非所有的JIT编译器的实现方式类似Listing4中伪码的实现。有一些JIT编译器实现为只有在成功运行构造方法之后才将instance变量置为non-null值,IBM JDK1.3和Sun JDK1.3都是这样实现的。但早期的版本以及其他的的JIT编辑器就很难保证了。并且除了这种 “out-of-order writes”问题,还会有其他的原因造成double-checked locking失败,你并不能确定你的代码将来运行在什么平台上面。总之,由于种种原因,double-checked locking其实只能是理论上的技术,并不能应用在实际的系统中。 三、结论 从上面的讨论可以看出,double-checked locking是不建议采用的,因为这样的代码不能确保在任何环境下都能够运行正常。由此可见你要么接受Listing 3的效率牺牲,要么采用Listing1的实现。如果你想不付出同步代价,Listing1的代码是比较好的选择了。 |
2.Re:浅析Singleton模式 [Re: fat32] | Copy to clipboard |
Posted by: Jove Posted on: 2003-05-10 00:01 Effective Java 第48条更详尽的分析了Singleton若干常见实现的错误 还给出一种 initialze-on-demand holder class idiom private static class FooHolder{ static final Foo foo=new Foo(); } public static Foo getFoo(){return FooHolder.foo;} 详见E文pdf或中文书 |
3.Re:浅析Singleton模式 [Re: fat32] | Copy to clipboard |
Posted by: mochow Posted on: 2003-05-10 00:09 关于这个《java与模式》这本书讲的也很不错 |
4.Re:浅析Singleton模式 [Re: fat32] | Copy to clipboard |
Posted by: fat32 Posted on: 2003-05-11 22:23 关于各种模式的讨论,各位能不能整理整理,发上来? 也算是自己整理思路啊 |
5.Re:浅析Singleton模式 [Re: fat32] | Copy to clipboard |
Posted by: Tomson Posted on: 2003-06-06 00:52 可不可以这样呢?
|
6.Re:浅析Singleton模式 [Re: Jove] | Copy to clipboard |
Posted by: richardluopeng Posted on: 2003-06-13 22:14 Jove wrote: 我想了解一下,这个有多大的意义(关于initialze-on-demand holder class idiom),谢谢! |
7.Re:浅析Singleton模式 [Re: richardluopeng] | Copy to clipboard |
Posted by: Jove Posted on: 2003-06-14 01:11 原文是 如果一个静态域的初始化非常昂贵,并且它也不见得会被用到,但一旦需要则会被充分地使用,那么这种情况下,initialize-on-demand holder class模式就非常合适 .. 此模式充分利用了Java语言中“只有当一个类被用到的时候才被初始化”[JLS,12.4.1]。 当getFoo方法第一次被调用时,它读入FooHolder.foo域,使得FooHolder类被初始化。 该模式的优美之处在于,getFoo方法并没有被同步,它只执行一次域访问,所以迟缓初始化并没有引入实际的访问开销。 这种模式的缺点在于,它不能用于实例域,只能用于静态域 |
Powered by Jute Powerful Forum® Version Jute 1.5.6 Ent Copyright © 2002-2021 Cjsdn Team. All Righits Reserved. 闽ICP备05005120号-1 客服电话 18559299278 客服信箱 714923@qq.com 客服QQ 714923 |