原文地址 譯者:丁一html
雙重檢查鎖定(如下稱爲DCL)已被普遍當作多線程環境下延遲初始化的一種高效手段。java
遺憾的是,在Java中,若是沒有額外的同步,它並不可靠。在其它語言中,如c++,實現DCL,須要依賴於處理器的內存模型、編譯器實行的重排序以及編譯器與同步庫之間的交互。因爲c++沒有對這些作出明確規定,很難說DCL是否有效。能夠在c++中使用顯式的內存屏障來使DCL生效,但Java中並無這些屏障。c++
來看下面的代碼編程
01 |
// Single threaded version |
02 |
class Foo { |
03 |
private Helper helper = null ; |
04 |
public Helper getHelper() { |
05 |
if (helper == null ) |
06 |
helper = new Helper(); |
07 |
return helper; |
08 |
} |
09 |
// other functions and members... |
10 |
} |
若是這段代碼用在多線程環境下,有幾個可能出錯的地方。最明顯的是,可能會建立出兩或多個Helper對象。(後面會提到其它問題)。將getHelper()方法改成同步便可修復此問題。緩存
01 |
// Correct multithreaded version |
02 |
class Foo { |
03 |
private Helper helper = null ; |
04 |
public synchronized Helper getHelper() { |
05 |
if (helper == null ) |
06 |
helper = new Helper(); |
07 |
return helper; |
08 |
} |
09 |
// other functions and members... |
10 |
} |
上面的代碼在每次調用getHelper時都會執行同步操做。DCL模式旨在消除helper對象被建立後還須要的同步。多線程
01 |
// Broken multithreaded version |
02 |
// "Double-Checked Locking" idiom |
03 |
class Foo { |
04 |
private Helper helper = null ; |
05 |
public Helper getHelper() { |
06 |
if (helper == null ) |
07 |
synchronized ( this ) { |
08 |
if (helper == null ) |
09 |
helper = new Helper(); |
10 |
} |
11 |
return helper; |
12 |
} |
13 |
// other functions and members... |
14 |
} |
不幸的是,這段代碼不管是在優化型的編譯器下仍是在共享內存處理器中都不能有效工做。併發
不起做用
上面代碼不起做用的緣由有不少。接下來咱們先說幾個比較顯而易見的緣由。理解這些以後,也許你想找出一種方法來「修復」DCL模式。你的修復也不會起做用:這裏面有很微妙的緣由。在理解了這些緣由以後,可能想進一步進行修復,但仍不會正常工做,由於存在更微妙的緣由。app
不少聰明的人在這上面花費了不少時間。除了在每一個線程訪問helper對象時執行鎖操道別無他法。ide
不起做用的第一個緣由
最顯而易見的緣由是,Helper對象初始化時的寫操做與寫入helper字段的操做能夠是無序的。這樣的話,若是某個線程調用getHelper()可能看到helper字段指向了一個Helper對象,但看到該對象裏的字段值倒是默認值,而不是在Helper構造方法裏設置的那些值。post
若是編譯器將調用內聯到構造方法中,那麼,若是編譯器能證實構造方法不會拋出異常或執行同步操做,初始化對象的這些寫操做與hepler字段的寫操做之間就能自由的重排序。
即使編譯器不對這些寫操做重排序,在多處理器上,某個處理器或內存系統也可能重排序這些寫操做,運行在其它 處理器上的線程就可能看到重排序帶來的結果。
Doug Lea寫了一篇更詳細的有關編譯器重排序的文章。
展現其不起做用的測試案例
Paul Jakubik找到了一個使用DCL不能正常工做的例子。下面的代碼作了些許整理:
001 |
public class DoubleCheckTest |
002 |
{ |
003 |
|
004 |
005 |
// static data to aid in creating N singletons |
006 |
static final Object dummyObject = new Object(); // for reference init |
007 |
static final int A_VALUE = 256 ; // value to initialize 'a' to |
008 |
static final int B_VALUE = 512 ; // value to initialize 'b' to |
009 |
static final int C_VALUE = 1024 ; |
010 |
static ObjectHolder[] singletons; // array of static references |
011 |
static Thread[] threads; // array of racing threads |
012 |
static int threadCount; // number of threads to create |
013 |
static int singletonCount; // number of singletons to create |
014 |
|
015 |
016 |
static volatile int recentSingleton; |
017 |
018 |
019 |
// I am going to set a couple of threads racing, |
020 |
// trying to create N singletons. Basically the |
021 |
// race is to initialize a single array of |
022 |
// singleton references. The threads will use |
023 |
// double checked locking to control who |
024 |
// initializes what. Any thread that does not |
025 |
// initialize a particular singleton will check |
026 |
// to see if it sees a partially initialized view. |
027 |
// To keep from getting accidental synchronization, |
028 |
// each singleton is stored in an ObjectHolder |
029 |
// and the ObjectHolder is used for |
030 |
// synchronization. In the end the structure |
031 |
// is not exactly a singleton, but should be a |
032 |
// close enough approximation. |
033 |
// |
034 |
035 |
036 |
// This class contains data and simulates a |
037 |
// singleton. The static reference is stored in |
038 |
// a static array in DoubleCheckFail. |
039 |
static class Singleton |
040 |
{ |
041 |
public int a; |
042 |
public int b; |
043 |
public int c; |
044 |
public Object dummy; |
045 |
046 |
public Singleton() |
047 |
{ |
048 |
a = A_VALUE; |
049 |
b = B_VALUE; |
050 |
c = C_VALUE; |
051 |
dummy = dummyObject; |
052 |
} |
053 |
} |
054 |
055 |
static void checkSingleton(Singleton s, int index) |
056 |
{ |
057 |
int s_a = s.a; |
058 |
int s_b = s.b; |
059 |
int s_c = s.c; |
060 |
Object s_d = s.dummy; |
061 |
if (s_a != A_VALUE) |
062 |
System.out.println( "[" + index + "] Singleton.a not initialized " + |
063 |
s_a); |
064 |
if (s_b != B_VALUE) |
065 |
System.out.println( "[" + index |
066 |
+ "] Singleton.b not intialized " + s_b); |
067 |
|
068 |
if (s_c != C_VALUE) |
069 |
System.out.println( "[" + index |
070 |
+ "] Singleton.c not intialized " + s_c); |
071 |
|
072 |
if (s_d != dummyObject) |
073 |
if (s_d == null ) |
074 |
System.out.println( "[" + index |
075 |
+ "] Singleton.dummy not initialized," |
076 |
+ " value is null" ); |
077 |
else |
078 |
System.out.println( "[" + index |
079 |
+ "] Singleton.dummy not initialized," |
080 |
+ " value is garbage" ); |
081 |
} |
082 |
083 |
// Holder used for synchronization of |
084 |
// singleton initialization. |
085 |
static class ObjectHolder |
086 |
{ |
087 |
public Singleton reference; |
088 |
} |
089 |
090 |
static class TestThread implements Runnable |
091 |
{ |
092 |
public void run() |
093 |
{ |
094 |
for ( int i = 0 ; i < singletonCount; ++i) |
095 |
{ |
096 |
ObjectHolder o = singletons[i]; |
097 |
if (o.reference == null ) |
098 |
{ |
099 |
synchronized (o) |
100 |
{ |
101 |
if (o.reference == null ) { |
102 |
o.reference = new Singleton(); |
103 |
recentSingleton = i; |
104 |
} |
105 |
// shouldn't have to check singelton here |
106 |
// mutex should provide consistent view |
107 |
} |
108 |
} |
109 |
else { |
110 |
checkSingleton(o.reference, i); |
111 |
int j = recentSingleton- 1 ; |
112 |
if (j > i) i = j; |
113 |
} |
114 |
} |
115 |
} |
116 |
} |
117 |
118 |
public static void main(String[] args) |
119 |
{ |
120 |
if ( args.length != 2 ) |
121 |
{ |
122 |
System.err.println( "usage: java DoubleCheckFail" + |
123 |
" <numThreads> <numSingletons>" ); |
124 |
} |
125 |
// read values from args |
126 |
threadCount = Integer.parseInt(args[ 0 ]); |
127 |
singletonCount = Integer.parseInt(args[ 1 ]); |
128 |
|
129 |
// create arrays |
130 |
threads = new Thread[threadCount]; |
131 |
singletons = new ObjectHolder[singletonCount]; |
132 |
133 |
// fill singleton array |
134 |
for ( int i = 0 ; i < singletonCount; ++i) |
135 |
singletons[i] = new ObjectHolder(); |
136 |
137 |
// fill thread array |
138 |
for ( int i = 0 ; i < threadCount; ++i) |
139 |
threads[i] = new Thread( new TestThread() ); |
140 |
141 |
// start threads |
142 |
for ( int i = 0 ; i < threadCount; ++i) |
143 |
threads[i].start(); |
144 |
145 |
// wait for threads to finish |
146 |
for ( int i = 0 ; i < threadCount; ++i) |
147 |
{ |
148 |
try |
149 |
{ |
150 |
System.out.println( "waiting to join " + i); |
151 |
threads[i].join(); |
152 |
} |
153 |
catch (InterruptedException ex) |
154 |
{ |
155 |
System.out.println( "interrupted" ); |
156 |
} |
157 |
} |
158 |
System.out.println( "done" ); |
159 |
} |
160 |
} |
當上述代碼運行在使用Symantec JIT的系統上時,不能正常工做。尤爲是,Symantec JIT將
1 |
singletons[i].reference = new Singleton(); |
編譯成了下面這個樣子(Symantec JIT用了一種基於句柄的對象分配系統)。
0206106A mov eax,0F97E78h 0206106F call 01F6B210 ; allocate space for ; Singleton, return result in eax 02061074 mov dword ptr [ebp],eax ; EBP is &singletons[i].reference ; store the unconstructed object here. 02061077 mov ecx,dword ptr [eax] ; dereference the handle to ; get the raw pointer 02061079 mov dword ptr [ecx],100h ; Next 4 lines are 0206107F mov dword ptr [ecx+4],200h ; Singleton's inlined constructor 02061086 mov dword ptr [ecx+8],400h 0206108D mov dword ptr [ecx+0Ch],0F84030h
如你所見,賦值給singletons[i].reference的操做在Singleton構造方法以前作掉了。在現有的Java內存模型下這徹底是容許的,在c和c++中也是合法的(由於c/c++都沒有內存模型(譯者注:這篇文章寫做時間較久,c++11已經有內存模型了))。
一種不起做用的「修復」
基於前文解釋的緣由,一些人提出了下面的代碼:
01 |
// (Still) Broken multithreaded version |
02 |
// "Double-Checked Locking" idiom |
03 |
class Foo { |
04 |
private Helper helper = null ; |
05 |
public Helper getHelper() { |
06 |
if (helper == null ) { |
07 |
Helper h; |
08 |
synchronized ( this ) { |
09 |
h = helper; |
10 |
if (h == null ) |
11 |
synchronized ( this ) { |
12 |
h = new Helper(); |
13 |
} // release inner synchronization lock |
14 |
helper = h; |
15 |
} |
16 |
} |
17 |
return helper; |
18 |
} |
19 |
// other functions and members... |
20 |
} |
將建立Helper對象的代碼放到了一個內部的同步塊中。直覺的想法是,在退出同步塊的時候應該有一個內存屏障,這會阻止Helper的初始化與helper字段賦值之間的重排序。
很不幸,這種直覺徹底錯了。同步的規則不是這樣的。monitorexit(即,退出同步塊)的規則是,在monitorexit前面的action必須在該monitor釋放以前執行。可是,並無哪裏有規定說monitorexit後面的action不能夠在monitor釋放以前執行。所以,編譯器將賦值操做helper = h;挪到同步塊裏面是很是合情合理的,這就回到了咱們以前說到的問題上。許多處理器提供了這種單向的內存屏障指令。若是改變鎖釋放的語義 —— 釋放時執行一個雙向的內存屏障 —— 將會帶來性能損失。
更多不起做用的「修復」
能夠作些事情迫使寫操做的時候執行一個雙向的內存屏障。這是很是重量級和低效的,且幾乎能夠確定一旦Java內存模型修改就不能正確工做了。不要這麼用。若是對此感興趣,我在另外一個網頁上描述了這種技術。不要使用它。
可是,即便初始化helper對象的線程用了雙向的內存屏障,仍然不起做用。
問題在於,在某些系統上,看到helper字段是非null的線程也須要執行內存屏障。
爲什麼?由於處理器有本身本地的對內存的緩存拷貝。在有些處理器上,除非處理器執行一個cache coherence指令(即,一個內存屏障),不然讀操做可能從過時的本地緩存拷貝中取值,即便其它處理器使用了內存屏障將它們的寫操做寫回了內存。
我開了另外一個頁面來討論這在Alpha處理器上是如何發生的。
值得費這麼大勁嗎?
對於大部分應用來講,將getHelper()變成同步方法的代價並不高。只有當你知道這確實形成了很大的應用開銷時才應該考慮這種細節的優化。
一般,更高級別的技巧,如,使用內部的歸併排序,而不是交換排序(見SPECJVM DB的基準),帶來的影響更大。
讓靜態單例生效
若是你要建立的是static單例對象(即,只會建立一個Helper對象),這裏有個簡單優雅的解決方案。
只需將singleton變量做爲另外一個類的靜態字段。Java的語義保證該字段被引用前是不會被初始化的,且任一訪問該字段的線程都會看到由初始化該字段所引起的全部寫操做。
1 |
class HelperSingleton { |
2 |
static Helper singleton = new Helper(); |
3 |
} |
對32位的基本類型變量DCL是有效的
雖然DCL模式不能用於對象引用,但能夠用於32位的基本類型變量。注意,DCL也不能用於對long和double類型的基本變量,由於不能保證未同步的64位基本變量的讀寫是原子操做。
01 |
// Correct Double-Checked Locking for 32-bit primitives |
02 |
class Foo { |
03 |
private int cachedHashCode = 0 ; |
04 |
public int hashCode() { |
05 |
int h = cachedHashCode; |
06 |
if (h == 0 ) |
07 |
synchronized ( this ) { |
08 |
if (cachedHashCode != 0 ) return cachedHashCode; |
09 |
h = computeHashCode(); |
10 |
cachedHashCode = h; |
11 |
} |
12 |
return h; |
13 |
} |
14 |
// other functions and members... |
15 |
} |
事實上,若是computeHashCode方法老是返回相同的結果且沒有其它附屬做用時(即,computeHashCode是個冪等方法),甚至能夠消除這裏的全部同步。
01 |
// Lazy initialization 32-bit primitives |
02 |
// Thread-safe if computeHashCode is idempotent |
03 |
class Foo { |
04 |
private int cachedHashCode = 0 ; |
05 |
public int hashCode() { |
06 |
int h = cachedHashCode; |
07 |
if (h == 0 ) { |
08 |
h = computeHashCode(); |
09 |
cachedHashCode = h; |
10 |
} |
11 |
return h; |
12 |
} |
13 |
// other functions and members... |
14 |
} |
用顯式的內存屏障使DCL有效
若是有顯式的內存屏障指令可用,則有可能使DCL生效。例如,若是你用的是C++,能夠參考來自Doug Schmidt等人所著書中的代碼:
01 |
// C++ implementation with explicit memory barriers |
02 |
// Should work on any platform, including DEC Alphas |
03 |
// From "Patterns for Concurrent and Distributed Objects", |
04 |
// by Doug Schmidt |
05 |
template < class TYPE, class LOCK> TYPE * |
06 |
Singleton<TYPE, LOCK>::instance ( void ) { |
07 |
// First check |
08 |
TYPE* tmp = instance_; |
09 |
// Insert the CPU-specific memory barrier instruction |
10 |
// to synchronize the cache lines on multi-processor. |
11 |
asm ( "memoryBarrier" ); |
12 |
if (tmp == 0) { |
13 |
// Ensure serialization (guard |
14 |
// constructor acquires lock_). |
15 |
Guard<LOCK> guard (lock_); |
16 |
// Double check. |
17 |
tmp = instance_; |
18 |
if (tmp == 0) { |
19 |
tmp = new TYPE; |
20 |
// Insert the CPU-specific memory barrier instruction |
21 |
// to synchronize the cache lines on multi-processor. |
22 |
asm ( "memoryBarrier" ); |
23 |
instance_ = tmp; |
24 |
} |
25 |
return tmp; |
26 |
} |
用線程局部存儲來修復DCL
Alexander Terekhov (TEREKHOV@de.ibm.com)提出了個能實現DCL的巧妙的作法 —— 使用線程局部存儲。每一個線程各自保存一個flag來表示該線程是否執行了同步。
01 |
class Foo { |
02 |
/** If perThreadInstance.get() returns a non-null value, this thread |
03 |
has done synchronization needed to see initialization |
04 |
of helper */ |
05 |
private final ThreadLocal perThreadInstance = new ThreadLocal(); |
06 |
private Helper helper = null ; |
07 |
public Helper getHelper() { |
08 |
if (perThreadInstance.get() == null ) createHelper(); |
09 |
return helper; |
10 |
} |
11 |
private final void createHelper() { |
12 |
synchronized ( this ) { |
13 |
if (helper == null ) |
14 |
helper = new Helper(); |
15 |
} |
16 |
// Any non-null value would do as the argument here |
17 |
perThreadInstance.set(perThreadInstance); |
18 |
} |
19 |
} |
這種方式的性能嚴重依賴於所使用的JDK實現。在Sun 1.2的實現中,ThreadLocal是很是慢的。在1.3中變得更快了,指望能在1.4上更上一個臺階。Doug Lea分析了一些延遲初始化技術實現的性能
在新的Java內存模型下
JDK5使用了新的Java內存模型和線程規範。
用volatile修復DCL
JDK5以及後續版本擴展了volatile語義,再也不容許volatile寫操做與其前面的讀寫操做重排序,也不容許volatile讀操做與其後面的讀寫操做重排序。更多詳細信息見Jeremy Manson的博客。
這樣,就能夠將helper字段聲明爲volatile來讓DCL生效。在JDK1.4或更早的版本里還是不起做用的。
01 |
// Works with acquire/release semantics for volatile |
02 |
// Broken under current semantics for volatile |
03 |
class Foo { |
04 |
private volatile Helper helper = null ; |
05 |
public Helper getHelper() { |
06 |
if (helper == null ) { |
07 |
synchronized ( this ) { |
08 |
if (helper == null ) |
09 |
helper = new Helper(); |
10 |
} |
11 |
} |
12 |
return helper; |
13 |
} |
14 |
} |
不可變對象的DCL
若是Helper是個不可變對象,那麼Helper中的全部字段都是final的,那麼不使用volatile也能使DCL生效。主要是由於指向不可變對象的引用應該表現出形如int和float同樣的行爲;讀寫不可變對象的引用是原子操做。
原創文章,轉載請註明: 轉載自併發編程網 – ifeve.com本文連接地址: 有關「雙重檢查鎖定失效」的說明