一、意图
在很多情况下我们要求一个类只有一个实例,例如数据库连接缓冲池类。我们需要一种机制来保证使用该类的程序能够比较方便的得到这个类的单一实例,并且要禁 止创建该类的第二个实例。解决这一问题的方法是让类自身保存它的唯一实例,这个类必须保证其他程序不能创建该类的其他实例,并且要提供访问这个唯一实例的 方法。这就是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的代码是比较好的选择了。
• 复习考试
在很多情况下我们要求一个类只有一个实例,例如数据库连接缓冲池类。我们需要一种机制来保证使用该类的程序能够比较方便的得到这个类的单一实例,并且要禁 止创建该类的第二个实例。解决这一问题的方法是让类自身保存它的唯一实例,这个类必须保证其他程序不能创建该类的其他实例,并且要提供访问这个唯一实例的 方法。这就是Singleton模式。
二、Singleton模式的Java实现
我们首先给出Singleton模式的Java实现样本:
Listing 1 :
package design.pattern.gof;
import java.util.Hashtable;
/**
* Singleton design pattern demo.
* Creation date: (2003-5-8 15:11:23)
* @author: Jecky Luo
*/
public class Singleton {
private static Singleton instance = new Singleton();
/**
* Singleton constructor comment.
*/
private Singleton() {
System.out.println("Singleton object created!");
}
public static Singleton getInstance() {
return instance;
}
}
首先这个类将自己的构造方法声明为private,这使得使用该类的程序无法创建这个类的实例。然后通过声明一个静态属性instance来保存该类的唯一实例并且提供了访问该单一实例的方法getInstance()。
很多资料上给出了类似如下的实现样本:
Listing 2 :
package design.pattern.gof;
import java.util.Hashtable;
/**
* Singleton design pattern demo.
* Creation date: (2003-5-8 15:11:23)
* @author: Jecky Luo
*/
public class Singleton {
private static Singleton instance;
/**
* Singleton constructor comment.
*/
private Singleton() {
System.out.println("Singleton object created!");
}
public static Singleton getInstance() {
if(instance == null){//1
instance = new Singleton();//2
}
return instance;//3
}
}
这种实现方式采用了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:
public static synchronized Singleton getInstance() {
if(instance == null){//1
instance = new Singleton();//2
}
return instance;//3
}
这段代码是很严谨的,通过synchronized关键字的使用,保证了在同一时刻只能有一个线程进入getInstance()方法。从而避免了 Listing2出现的问题。然而,我们只要仔细研究一下就会发现这段代码也并非完美,因为synchronized只有在getInstance()方 法第一次被调用时才有必要,而此后的所有调用都没有必要同步了,因为Singleton对象已经是non-null值,真正需要同步的//2代码再也不会 被执行到。然而因为整个方法被同步了,每次调用该方法你都要付出不必要的同步代价。早期的JVM实现,同步代价是很高的,虽然新版本的JVM已经有很大改 善,但作为一个优秀的程序员,你总要想让自己的代码完美,没有人愿意付出不必要的代价,因为这实在是一件很不爽的事。那么,我们再看下面的更改:
Listing4:
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {//1
if (instance == null)//2
instance = new Singleton();//3
}
}
return instance;
}
我们来看下面的事件序列:
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:
buffer = memalloc (); //为Singleton对象分配内存
instance = mem; //将内存指针付给instance变量,现在instance已经
//为非null,但还没有初始化
callSingleton(instance); //调用Singleton类的构造方法.
上面这段伪码首先将分配的内存付给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的代码是比较好的选择了。
fat32 edited on 2003-05-09 17:29
• 复习考试
作者 | Re:浅析Singleton模式 [Re:fat32] |
Jove ![]() CJSDN高级会员 ![]() 发贴: 1195 积分: 194 ![]() | ![]() ![]() ![]() ![]() ![]() ![]() 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或中文书 准备找一份新的工作,一份新的开始. 希望在上海做一个地道的Java Developer, 仅此而已 如有合适的机会,请与我Email联系 • JAVA/.net高级程序员招聘 |
作者 | Re:浅析Singleton模式 [Re:fat32] |
mochow ![]() Mary有只小山羊咿呀咿 ![]() ![]() ![]() ![]() ![]() 发贴: 339 积分: 40 ![]() | ![]() ![]() ![]() ![]() ![]() ![]() 关于这个《java与模式》这本书讲的也很不错 • 有没有用Lomboz实现了Bean管理的EntityBean |
作者 | Re:浅析Singleton模式 [Re:fat32] |
fat32 ![]() 一个字:帅!@_@ 版主 ![]() 发贴: 250 积分: 60 ![]() | ![]() ![]() ![]() ![]() ![]() ![]() 关于各种模式的讨论,各位能不能整理整理,发上来? 也算是自己整理思路啊 • [求助]CTabItem无法控制browser |