要理解双重检查锁定习语是从哪里v源的Q就必须理解通用单例创徏习语Q如清单 1 中的阐释Q?/p>
清单 1. 单例创徏习语
import java.util.*; class Singleton { private static Singleton instance; private Vector v; private boolean inUse; private Singleton() { v = new Vector(); v.addElement(new Object()); inUse = true; } public static Singleton getInstance() { if (instance == null) //1 instance = new Singleton(); //2 return instance; //3 } } |
此类的设计确保只创徏一?Singleton
对象。构造函数被声明?private
Q?code>getInstance() Ҏ只创Z个对象。这个实现适合于单U程E序。然而,当引入多U程Ӟ必通过同步来保?getInstance()
Ҏ。如果不保护 getInstance()
ҎQ则可能q回 Singleton
对象的两个不同的实例。假设两个线Eƈ发调?getInstance()
Ҏq且按以下顺序执行调用:
getInstance()
Ҏq决?instance
?//1 处ؓ null
?if
代码块,但在执行 //2 处的代码行时被线E?2 预占?getInstance()
Ҏq在 //1 处决?instance
?null
?if
代码块ƈ创徏一个新?Singleton
对象q在 //2 处将变量 instance
分配l这个新对象?Singleton
对象引用?br />
Singleton
对象?l果?getInstance()
Ҏ创徏了两?Singleton
对象Q而它本该只创Z个对象。通过同步 getInstance()
Ҏ从而在同一旉只允怸个线E执行代码,q个问题得以ҎQ如清单 2 所C:
public static synchronized Singleton getInstance() { if (instance == null) //1 instance = new Singleton(); //2 return instance; //3 } |
清单 2 中的代码针对多线E访?getInstance()
Ҏq行得很好。然而,当分析这D代码时Q您会意识到只有在第一ơ调用方法时才需要同步。由于只有第一ơ调用执行了 //2 处的代码Q而只有此行代码需要同步,因此无需对后l调用用同步。所有其他调用用于决?instance
是非 null
的,q将其返回。多U程能够安全q发地执行除W一ơ调用外的所有调用。尽如此,׃该方法是 synchronized
的,需要ؓ该方法的每一ơ调用付出同步的代hQ即使只有第一ơ调用需要同步?
Z此方法更为有效,一个被UCؓ双重查锁定的习语应q而生了。这个想法是Z避免寚wW一ơ调用外的所有调用都实行同步的昂贵代仗同步的代h在不同的 JVM 间是不同的。在早期Q代L当高。随着更高U的 JVM 的出玎ͼ同步的代价降低了Q但出入 synchronized
Ҏ或块仍然有性能损失。不考虑 JVM 技术的q步Q程序员们绝不想不必要地费处理旉?/p>
因ؓ只有清单 2 中的 //2 行需要同步,我们可以只将其包装到一个同步块中,如清?3 所C:
public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { instance = new Singleton(); } } return instance; } |
清单 3 中的代码展示了用多线E加以说明的和清?1 相同的问题。当 instance
?null
Ӟ两个U程可以q发地进?if
语句内部。然后,一个线E进?synchronized
块来初始?instance
Q而另一个线E则被阻断。当W一个线E退?synchronized
块时Q等待着的线E进入ƈ创徏另一?Singleton
对象。注意:当第二个U程q入 synchronized
块时Q它q没有检?instance
是否?null
?
![]() ![]() |
![]()
|
为处理清?3 中的问题Q我们需要对 instance
q行W二ơ检查。这是“双重查锁?#8221;名称的由来。将双重查锁定习语应用到清单 3 的结果就是清?4 ?
public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { //1 if (instance == null) //2 instance = new Singleton(); //3 } } return instance; } |
双重查锁定背后的理论是:?//2 处的W二ơ检查Q如清单 3 中那P创徏两个不同?Singleton
对象成ؓ不可能。假设有下列事g序列Q?
getInstance()
Ҏ?instance
?null
Q线E?1 ?//1 处进?synchronized
块?getInstance()
Ҏ?br />
instance
仍旧?null
Q线E?2 试图获取 //1 处的锁。然而,׃U程 1 持有该锁Q线E?2 ?//1 处阻塞?br />
null
Q线E?1 q创Z?Singleton
对象q将其引用赋值给 instance
?br />
synchronized
块ƈ?getInstance()
Ҏq回实例?instance
是否?null
?instance
是非 null
的,q没有创建第二个 Singleton
对象Q由U程 1 创徏的对象被q回?双重查锁定背后的理论是完的。不q地是,现实完全不同。双重检查锁定的问题是:q不能保证它会在单处理器或多处理器计机上顺利运行?/p>
双重查锁定失败的问题q不归咎?JVM 中的实现 bugQ而是归咎?Java q_内存模型。内存模型允许所谓的“无序写入”Q这也是q些习语p|的一个主要原因?/p>
![]() ![]() |
![]()
|
释该问题Q需要重新考察上述清单 4 中的 //3 行。此行代码创Z一?Singleton
对象q初始化变量 instance
来引用此对象。这行代码的问题是:?Singleton
构造函C执行之前Q变?instance
可能成ؓ?null
的?/p>
什么?q一说法可能让您始料未及Q但事实实如此。在解释q个现象如何发生前,请先暂时接受q一事实Q我们先来考察一下双重检查锁定是如何被破坏的。假设清?4 中代码执行以下事件序列:
getInstance()
Ҏ?br />
instance
?null
Q线E?1 ?//1 处进?synchronized
块?null
?null
。因为实例不?nullQ线E?2 ?instance
引用q回l一个构造完整但部分初始化了?Singleton
对象?Singleton
对象的构造函数ƈ引用返回给它,来完成对该对象的初始化?此事件序列发生在U程 2 q回一个尚未执行构造函数的对象的时候?/p>
为展C此事g的发生情况,假设Z码行 instance =new Singleton();
执行了下列伪代码Q?instance =new Singleton();
mem = allocate(); //Allocate memory for Singleton object. instance = mem; //Note that instance is now non-null, but //has not been initialized. ctorSingleton(instance); //Invoke constructor for Singleton passing //instance. |
q段伪代码不仅是可能的,而且是一?JIT ~译器上真实发生的。执行的序是颠倒的Q但鉴于当前的内存模型,q也是允许发生的。JIT ~译器的q一行ؓ使双重检查锁定的问题只不q是一ơ学术实践而已?/p>
明这一情况Q假设有清单 5 中的代码。它包含一个剥ȝ?getInstance()
Ҏ。我已经删除?#8220;双重查?#8221;以简化我们对生成的汇~代码(清单 6Q的回顾。我们只兛_ JIT ~译器如何编?instance=new Singleton();
代码。此外,我提供了一个简单的构造函数来明确说明汇编代码中该构造函数的q行情况?
class Singleton { private static Singleton instance; private boolean inUse; private int val; private Singleton() { inUse = true; val = 5; } public static Singleton getInstance() { if (instance == null) instance = new Singleton(); return instance; } } |
清单 6 包含?Sun JDK 1.2.1 JIT ~译器ؓ清单 5 中的 getInstance()
Ҏ体生成的汇编代码?
;asm code generated for getInstance 054D20B0 mov eax,[049388C8] ;load instance ref 054D20B5 test eax,eax ;test for null 054D20B7 jne 054D20D7 054D20B9 mov eax,14C0988h 054D20BE call 503EF8F0 ;allocate memory 054D20C3 mov [049388C8],eax ;store pointer in ;instance ref. instance ;non-null and ctor ;has not run 054D20C8 mov ecx,dword ptr [eax] 054D20CA mov dword ptr [ecx],1 ;inline ctor - inUse=true; 054D20D0 mov dword ptr [ecx+4],5 ;inline ctor - val=5; 054D20D7 mov ebx,dword ptr ds:[49388C8h] 054D20DD jmp 054D20B0 |
? 为引用下列说明中的汇~代码行Q我引用指令地址的最后两个|因ؓ它们都以 054D20
开头。例如,B5
代表 test eax,eax
?/p>
汇编代码是通过q行一个在无限循环中调?getInstance()
Ҏ的测试程序来生成的。程序运行时Q请q行 Microsoft Visual C++ 调试器ƈ其附到表示试E序?Java q程中。然后,中断执行q找到表C无限循环的汇~代码?/p>
B0
?B5
处的前两行汇~代码将 instance
引用从内存位|?049388C8
加蝲?eax
中,q进?null
查。这跟清?5 中的 getInstance()
Ҏ的第一行代码相对应。第一ơ调用此ҎӞinstance
?null
Q代码执行到 B9
?code>BE 处的代码?Singleton
对象从堆中分配内存,q将一个指向该块内存的指针存储?eax
中。下一行代码,C3
Q获?eax
中的指针q将其存储回内存位置?049388C8
的实例引用。结果是Q?code>instance 现在为非 null
q引用一个有效的 Singleton
对象。然而,此对象的构造函数尚未运行,q恰是破坏双重检查锁定的情况。然后,?C8
行处Q?code>instance 指针被解除引用ƈ存储?ecx
?code>CA ?D0
行表C内联的构造函敎ͼ该构造函数将?true
?5
存储?Singleton
对象。如果此代码在执?C3
行后且在完成该构造函数前被另一个线E中断,则双重检查锁定就会失败?/p>
不是所有的 JIT ~译器都生成如上代码。一些生成了代码Q从而只在构造函数执行后?instance
成ؓ?null
。针?Java 技术的 IBM SDK 1.3 版和 Sun JDK 1.3 都生成这L代码。然而,qƈ不意味着应该在这些实例中使用双重查锁定。该习语p|q有一些其他原因。此外,您ƈ不总能知道代码会在哪些 JVM 上运行,?JIT ~译器L会发生变化,从而生成破坏此习语的代码?/p>
![]() ![]() |
![]()
|
考虑到当前的双重查锁定不起作用,我加入了另一个版本的代码Q如清单 7 所C,从而防止您刚才看到的无序写入问题?
public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { //1 Singleton inst = instance; //2 if (inst == null) { synchronized(Singleton.class) { //3 inst = new Singleton(); //4 } instance = inst; //5 } } } return instance; } |
看着清单 7 中的代码Q您应该意识C情变得有点荒谬。请CQ创建双重检查锁定是Z避免对简单的三行 getInstance()
Ҏ实现同步。清?7 中的代码变得难于控制。另外,该代码没有解决问题。仔l检查可h原因?/p>
此代码试N免无序写入问题。它试图通过引入局部变?inst
和第二个 synchronized
块来解决q一问题。该理论实现如下Q?
getInstance()
Ҏ?br />
instance
?null
Q线E?1 ?//1 处进入第一?synchronized
块?inst
获取 instance
的|该值在 //2 处ؓ null
?inst
?null
Q线E?1 ?//3 处进入第二个 synchronized
块?inst
为非 null
Q但?Singleton
的构造函数执行前。(q就是我们刚才看到的无序写入问题。) getInstance()
Ҏ?br />
instance
?null
Q线E?2 试图?//1 处进入第一?synchronized
块。由于线E?1 目前持有此锁Q线E?2 被阻断?br />
Singleton
对象?//5 处赋值给变量 instance
Qƈ退两个 synchronized
块?instance
?br />
instance
赋值给 inst
?br />
instance
为非 null
Q将其返回?q里的关键行?//5。此行应该确?instance
只ؓ null
或引用一个构造完整的 Singleton
对象。该问题发生在理论和实际彼此背道而驰的情况下?/p>
׃当前内存模型的定义,清单 7 中的代码无效。Java 语言规范QJava Language SpecificationQJLSQ要求不能将 synchronized
块中的代码移出来。但是,q没有说不能?synchronized
块外面的代码U?em>?/em> synchronized
块中?
JIT ~译器会在这里看C个优化的Z。此优化会删?//4 ?//5 处的代码Q组合ƈ且生成清?8 中所C的代码?
public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { //1 Singleton inst = instance; //2 if (inst == null) { synchronized(Singleton.class) { //3 //inst = new Singleton(); //4 instance = new Singleton(); } //instance = inst; //5 } } } return instance; } |
如果q行此项优化Q您同样遇到我们之前讨的无序写入问题?/p>
![]() ![]() |
![]()
|
另一个想法是针对变量 W二点值得展开讨论。假设有清单 9 中的代码Q?/p>
inst
以及 instance
使用关键?volatile
。根?JLSQ参?参考资?/a>Q,声明?volatile
的变量被认ؓ是顺序一致的Q即Q不是重新排序的。但是试图?volatile
来修正双重检查锁定的问题Q会产生以下两个问题Q?/p>
volatile
?
清单 9. 使用?volatile 的顺序一致?/strong>
class test { private volatile boolean stop = false; private volatile int num = 0; public void foo() { num = 100; //This can happen second stop = true; //This can happen first //... } public void bar() { if (stop) num += num; //num can == 0! } //... } |
Ҏ JLSQ由?stop
?num
被声明ؓ volatile
Q它们应该顺序一致。这意味着如果 stop
曄?true
Q?code>num 一定曾被设|成 100
。尽如此,因ؓ许多 JVM 没有实现 volatile
的顺序一致性功能,您就不能依赖此行为。因此,如果U程 1 调用 foo
q且U程 2 q发地调?bar
Q则U程 1 可能?num
被设|成?100
之前?stop
讄?true
。这导致线E见?stop
?true
Q?num
仍被讄?0
。?volatile
?64 位变量的原子数还有另外一些问题,但这已超Z本文的讨围。有x主题的更多信息,请参?参考资?/a>?/p>
![]() ![]() |
![]()
|
底线是Q无Z何种形式Q都不应使用双重查锁定,因ؓ您不能保证它在Q?JVM 实现上都能顺利运行。JSR-133 是有兛_存模型寻址问题的,管如此Q新的内存模型也不会支持双重查锁定。因此,您有两种选择Q?/p>
getInstance()
Ҏ的同步?br />
static
字段?选择?2 如清?10 中所C?/p>
清单 10. 使用 static 字段的单例实?/strong>
class Singleton { private Vector v; private boolean inUse; private static Singleton instance = new Singleton(); private Singleton() { v = new Vector(); inUse = true; //... } public static Singleton getInstance() { return instance; } } |
清单 10 的代码没有用同步,q且保调用 static getInstance()
Ҏ时才创徏 Singleton
。如果您的目标是消除同步Q则q将是一个很好的选择?/p>
![]() ![]() |
![]()
|
鉴于无序写入和引用在构造函数执行前变成?null
的问题,您可能会考虑 String
cR假设有下列代码Q?/p>
private String str; //... str = new String("hello"); |
String
cd该是不变的。尽如此,鉴于我们之前讨论的无序写入问题,那会在这里导致问题吗Q答案是肯定的。考虑两个U程讉K String str
。一个线E能看见 str
引用一?String
对象Q在该对象中构造函数尚未运行。事实上Q清?11 包含展示q种情况发生的代码。注意,q个代码仅在我测试用的旧?JVM 上会p|。IBM 1.3 ?Sun 1.3 JVM 都会如期生成不变?String
?/p>
清单 11. 可变 String 的例?/strong>
class StringCreator extends Thread { MutableString ms; public StringCreator(MutableString muts) { ms = muts; } public void run() { while(true) ms.str = new String("hello"); //1 } } class StringReader extends Thread { MutableString ms; public StringReader(MutableString muts) { ms = muts; } public void run() { while(true) { if (!(ms.str.equals("hello"))) //2 { System.out.println("String is not immutable!"); break; } } } } class MutableString { public String str; //3 public static void main(String args[]) { MutableString ms = new MutableString(); //4 new StringCreator(ms).start(); //5 new StringReader(ms).start(); //6 } } |
此代码在 //4 处创Z?MutableString
c,它包含了一?String
引用Q此引用?//3 处的两个U程׃n。在?//5 ?//6 处,在两个分开的线E上创徏了两个对?StringCreator
?StringReader
。传入一?MutableString
对象的引用?code>StringCreator c进入到一个无限@环中q且使用?#8220;hello”?//1 处创?String
对象?code>StringReader 也进入到一个无限@环中Qƈ且在 //2 处检查当前的 String
对象的值是不是 “hello”。如果不行,StringReader
U程打印Z条消息ƈ停止。如?String
cL不变的,则从此程序应当看不到M输出。如果发生了无序写入问题Q则?StringReader
看到 str
引用的惟一Ҏl不是gؓ“hello”?String
对象?/p>
在旧版的 JVM ?Sun JDK 1.2.1 上运行此代码会导致无序写入问题。ƈ因此D一个非不变?String
?/p>
![]() ![]() |
![]()
|
为避免单例中代h高昂的同步,E序员非常聪明地发明了双重检查锁定习语。不q的是,鉴于当前的内存模型的原因Q该习语未得到q泛使用Q就明显成ؓ了一U不安全的编E结构。重定义脆弱的内存模型这一领域的工作正在进行中。尽如此,即是在新提议的内存模型中,双重查锁定也是无效的。对此问题最佳的解决Ҏ是接受同步或者用一?static field
?
单态定?
Singleton模式主要作用是保证在Java应用E序中,一个类Class只有一个实例存在?/p>
在很多操作中Q比如徏立目?数据库连接都需要这L单线E操作?/p>
q有, singleton能够被状态化; q样Q多个单态类在一起就可以作ؓ一个状态仓库一样向外提供服务,比如Q你要论坛中的帖子计数器Q每ơ浏览一ơ需要计敎ͼ单态类能否保持住这个计敎ͼq且能synchronize的安全自动加1Q如果你要把q个数字怹保存到数据库Q你可以在不修改单态接口的情况下方便的做到?/p>
另外斚wQSingleton也能够被无状态化。提供工h质的功能,
Singleton模式׃ؓ我们提供了这样实现的可能。用Singleton的好处还在于可以节省内存Q因为它限制了实例的个数Q有利于Java垃圾回收Qgarbage collectionQ?/p>
我们常常看到工厂模式中类装入?class loader)中也用Singleton模式实现?因ؓ被装入的cd际也属于资源?/p>
如何使用?
一般Singleton模式通常有几UŞ?
public class Singleton {
private Singleton(){}
//在自己内部定义自׃个实例,是不是很奇怪?
//注意q是private 只供内部调用
private static Singleton instance = new Singleton();
//q里提供了一个供外部讉K本class的静态方法,可以直接讉K
public static Singleton getInstance() {
return instance;
}
}
W二UŞ?
public class Singleton {
private static Singleton instance = null;
public static synchronized Singleton getInstance() {
//q个Ҏ比上面有所改进Q不用每ơ都q行生成对象Q只是第一ơ
//使用时生成实例,提高了效率!
if (instance==null)
instanceQnew Singleton();
return instance; }
}
使用Singleton.getInstance()可以讉K单态类?/p>
上面W二中Ş式是lazy initializationQ也是说第一ơ调用时初始SingletonQ以后就不用再生成了?/p>
注意到lazy initialization形式中的synchronizedQ这个synchronized很重要,如果没有synchronizedQ那么用getInstance()是有可能得到多个Singleton实例。关于lazy initialization的Singleton有很多涉及double-checked locking (DCL)的讨论,有兴者进一步研I?/p>
一般认为第一UŞ式要更加安全些?/p>
使用Singleton注意事项Q?/p>
有时在某些情况下Q用Singletonq不能达到Singleton的目的,如有多个Singleton对象同时被不同的c装入器装蝲Q在EJBq样的分布式pȝ中用也要注意这U情况,因ؓEJB是跨服务器,跨JVM的?/p>
我们以SUN公司的宠物店源码(Pet Store 1.3.1)的ServiceLocatorZE微分析一下:
在Pet Store中ServiceLocator有两U,一个是EJB目录下;一个是WEB目录下,我们查这两个ServiceLocator会发现内容差不多Q都是提供EJB的查询定位服务,可是Z么要分开呢?仔细研究对这两种ServiceLocator才发现区别:在WEB中的ServiceLocator的采取Singleton模式QServiceLocator属于资源定位Q理所当然应该使用Singleton模式。但是在EJB中,Singleton模式已经失去作用Q所以ServiceLocator才分成两U,一U面向WEB服务的,一U是面向EJB服务的?/p>
Singleton模式看v来简单,使用Ҏ也很方便Q但是真正用好,是非怸ҎQ需要对Java的类 U程内存{概忉|相当的了解?/p>
MQ如果你的应用基于容器,那么Singleton模式用或者不用,可以使用相关替代技术?/p>