雙重檢查鎖定(以下稱為DCL)已被廣泛當(dāng)做多線程環(huán)境下延遲初始化的一種高效手段。
遺憾的是,在Java中,如果沒有額外的同步,它并不可靠。在其它語言中,如c++,實現(xiàn)DCL,需要依賴于處理器的內(nèi)存模型、編譯器實行的重排序以及編 譯器與同步庫之間的交互。由于c++沒有對這些做出明確規(guī)定,很難說DCL是否有效。可以在c++中使用顯式的內(nèi)存屏障來使DCL生效,但Java中并沒 有這些屏障。
來看下面的代碼
| 01 | // Single threaded version |
| 03 | ??private Helper helper = null; |
| 04 | ??public Helper getHelper() { |
| 05 | ????if (helper == null) |
| 06 | ????????helper = new Helper(); |
| 09 | ??// other functions and members... |
如果這段代碼用在多線程環(huán)境下,有幾個可能出錯的地方。最明顯的是,可能會創(chuàng)建出兩或多個Helper對象。(后面會提到其它問題)。將getHelper()方法改為同步即可修復(fù)此問題。
| 01 | // Correct multithreaded version |
| 03 | ??private Helper helper = null; |
| 04 | ??public synchronized Helper getHelper() { |
| 05 | ????if (helper == null) |
| 06 | ????????helper = new Helper(); |
| 09 | ??// other functions and members... |
上面的代碼在每次調(diào)用getHelper時都會執(zhí)行同步操作。DCL模式旨在消除helper對象被創(chuàng)建后還需要的同步。
| 01 | // Broken multithreaded version |
| 02 | // "Double-Checked Locking" idiom |
| 04 | ??private Helper helper = null; |
| 05 | ??public Helper getHelper() { |
| 06 | ????if (helper == null) |
| 07 | ??????synchronized(this) { |
| 08 | ????????if (helper == null) |
| 09 | ??????????helper = new Helper(); |
| 13 | ??// other functions and members... |
不幸的是,這段代碼無論是在優(yōu)化型的編譯器下還是在共享內(nèi)存處理器中都不能有效工作。
不起作用
上面代碼不起作用的原因有很多。接下來我們先說幾個比較顯而易見的原因。理解這些之后,也許你想找出一種方法來“修復(fù)”DCL模式。你的修復(fù)也不會起作用:這里面有很微妙的原因。在理解了這些原因之后,可能想進一步進行修復(fù),但仍不會正常工作,因為存在更微妙的原因。
很多聰明的人在這上面花費了很多時間。除了在每個線程訪問helper對象時執(zhí)行鎖操作別無他法。
不起作用的第一個原因
最顯而易見的原因是,Helper對象初始化時的寫操作與寫入helper字段的操作可以是無序的。這樣的話,如果某個線程調(diào)用getHelper()可 能看到helper字段指向了一個Helper對象,但看到該對象里的字段值卻是默認值,而不是在Helper構(gòu)造方法里設(shè)置的那些值。
如果編譯器將調(diào)用內(nèi)聯(lián)到構(gòu)造方法中,那么,如果編譯器能證明構(gòu)造方法不會拋出異常或執(zhí)行同步操作,初始化對象的這些寫操作與hepler字段的寫操作之間就能自由的重排序。
即便編譯器不對這些寫操作重排序,在多處理器上,某個處理器或內(nèi)存系統(tǒng)也可能重排序這些寫操作,運行在其它 處理器上的線程就可能看到重排序帶來的結(jié)果。
Doug Lea寫了一篇更詳細的有關(guān)編譯器重排序的文章。
展示其不起作用的測試案例
Paul Jakubik找到了一個使用DCL不能正常工作的例子。下面的代碼做了些許整理:
| 001 | public class DoubleCheckTest |
| 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 |
| 016 | ??static volatile int recentSingleton; |
| 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. |
| 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 |
| 044 | ????public Object dummy; |
| 046 | ????public Singleton() |
| 051 | ??????dummy = dummyObject; |
| 055 | ??static void checkSingleton(Singleton s, int index) |
| 060 | ????Object s_d = s.dummy; |
| 061 | ????if(s_a != A_VALUE) |
| 062 | ??????System.out.println("[" + index + "] Singleton.a not initialized " + |
| 064 | ????if(s_b != B_VALUE) |
| 065 | ??????System.out.println("[" + index |
| 066 | ?????????????????????????+ "] Singleton.b not intialized " + s_b); |
| 068 | ????if(s_c != C_VALUE) |
| 069 | ??????System.out.println("[" + index |
| 070 | ?????????????????????????+ "] Singleton.c not intialized " + s_c); |
| 072 | ????if(s_d != dummyObject) |
| 073 | ??????if(s_d == null) |
| 074 | ????????System.out.println("[" + index |
| 075 | ???????????????????????????+ "] Singleton.dummy not initialized," |
| 076 | ???????????????????????????+ " value is null"); |
| 078 | ????????System.out.println("[" + index |
| 079 | ???????????????????????????+ "] Singleton.dummy not initialized," |
| 080 | ???????????????????????????+ " value is garbage"); |
| 083 | ??// Holder used for synchronization of |
| 084 | ??// singleton initialization. |
| 085 | ??static class ObjectHolder |
| 087 | ????public Singleton reference; |
| 090 | ??static class TestThread implements Runnable |
| 092 | ????public void run() |
| 094 | ??????for(int i = 0; i < singletonCount; ++i) |
| 096 | ????ObjectHolder o = singletons[i]; |
| 097 | ????????if(o.reference == null) |
| 099 | ??????????synchronized(o) |
| 101 | ????????????if (o.reference == null) { |
| 102 | ??????????????o.reference = new Singleton(); |
| 103 | ??????????recentSingleton = i; |
| 105 | ????????????// shouldn't have to check singelton here |
| 106 | ????????????// mutex should provide consistent view |
| 110 | ??????????checkSingleton(o.reference, i); |
| 111 | ??????int j = recentSingleton-1; |
| 112 | ??????if (j > i) i = j; |
| 118 | ??public static void main(String[] args) |
| 120 | ????if( args.length != 2 ) |
| 122 | ??????System.err.println("usage: java DoubleCheckFail" + |
| 123 | ?????????????????????????" <numThreads> <numSingletons>"); |
| 125 | ????// read values from args |
| 126 | ????threadCount = Integer.parseInt(args[0]); |
| 127 | ????singletonCount = Integer.parseInt(args[1]); |
| 130 | ????threads = new Thread[threadCount]; |
| 131 | ????singletons = new ObjectHolder[singletonCount]; |
| 133 | ????// fill singleton array |
| 134 | ????for(int i = 0; i < singletonCount; ++i) |
| 135 | ??????singletons[i] = new ObjectHolder(); |
| 137 | ????// fill thread array |
| 138 | ????for(int i = 0; i < threadCount; ++i) |
| 139 | ??????threads[i] = new Thread( new TestThread() ); |
| 142 | ????for(int i = 0; i < threadCount; ++i) |
| 143 | ??????threads[i].start(); |
| 145 | ????// wait for threads to finish |
| 146 | ????for(int i = 0; i < threadCount; ++i) |
| 150 | ????????System.out.println("waiting to join " + i); |
| 151 | ????????threads[i].join(); |
| 153 | ??????catch(InterruptedException ex) |
| 155 | ????????System.out.println("interrupted"); |
| 158 | ????System.out.println("done"); |
當(dāng)上述代碼運行在使用Symantec JIT的系統(tǒng)上時,不能正常工作。尤其是,Symantec JIT將
| 1 | singletons[i].reference = new Singleton(); |
編譯成了下面這個樣子(Symantec JIT用了一種基于句柄的對象分配系統(tǒng))。
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構(gòu)造方法之前做掉了。在現(xiàn)有的Java內(nèi)存模型下這完全是允許的,在c和c++中也是合法的(因為c/c++都沒有內(nèi)存模型(譯者注:這篇文章寫作時間較久,c++11已經(jīng)有內(nèi)存模型了))。
一種不起作用的“修復(fù)”
基于前文解釋的原因,一些人提出了下面的代碼:
| 01 | // (Still) Broken multithreaded version |
| 02 | // "Double-Checked Locking" idiom |
| 04 | ??private Helper helper = null; |
| 05 | ??public Helper getHelper() { |
| 06 | ????if (helper == null) { |
| 08 | ??????synchronized(this) { |
| 10 | ????????if (h == null) |
| 11 | ????????????synchronized (this) { |
| 12 | ??????????????h = new Helper(); |
| 13 | ????????????} // release inner synchronization lock |
| 19 | ??// other functions and members... |
將創(chuàng)建Helper對象的代碼放到了一個內(nèi)部的同步塊中。直覺的想法是,在退出同步塊的時候應(yīng)該有一個內(nèi)存屏障,這會阻止Helper的初始化與helper字段賦值之間的重排序。
很不幸,這種直覺完全錯了。同步的規(guī)則不是這樣的。monitorexit(即,退出同步塊)的規(guī)則是,在monitorexit前面的action必須 在該monitor釋放之前執(zhí)行。但是,并沒有哪里有規(guī)定說monitorexit后面的action不可以在monitor釋放之前執(zhí)行。因此,編譯器 將賦值操作helper = h;挪到同步塊里面是非常合情合理的,這就回到了我們之前說到的問題上。許多處理器提供了這種單向的內(nèi)存屏障指令。如果改變鎖釋放的語義 —— 釋放時執(zhí)行一個雙向的內(nèi)存屏障 —— 將會帶來性能損失。
更多不起作用的“修復(fù)”
可以做些事情迫使寫操作的時候執(zhí)行一個雙向的內(nèi)存屏障。這是非常重量級和低效的,且?guī)缀蹩梢钥隙ㄒ坏㎎ava內(nèi)存模型修改就不能正確工作了。不要這么用。如果對此感興趣,我在另一個網(wǎng)頁上描述了這種技術(shù)。不要使用它。
但是,即使初始化helper對象的線程用了雙向的內(nèi)存屏障,仍然不起作用。
問題在于,在某些系統(tǒng)上,看到helper字段是非null的線程也需要執(zhí)行內(nèi)存屏障。
為何?因為處理器有自己本地的對內(nèi)存的緩存拷貝。在有些處理器上,除非處理器執(zhí)行一個cache coherence指令(即,一個內(nèi)存屏障),否則讀操作可能從過期的本地緩存拷貝中取值,即使其它處理器使用了內(nèi)存屏障將它們的寫操作寫回了內(nèi)存。
我開了另一個頁面來討論這在Alpha處理器上是如何發(fā)生的。
值得費這么大勁嗎?
對于大部分應(yīng)用來說,將getHelper()變成同步方法的代價并不高。只有當(dāng)你知道這確實造成了很大的應(yīng)用開銷時才應(yīng)該考慮這種細節(jié)的優(yōu)化。
通常,更高級別的技巧,如,使用內(nèi)部的歸并排序,而不是交換排序(見SPECJVM DB的基準(zhǔn)),帶來的影響更大。
讓靜態(tài)單例生效
如果你要創(chuàng)建的是static單例對象(即,只會創(chuàng)建一個Helper對象),這里有個簡單優(yōu)雅的解決方案。
只需將singleton變量作為另一個類的靜態(tài)字段。Java的語義保證該字段被引用前是不會被初始化的,且任一訪問該字段的線程都會看到由初始化該字段所引發(fā)的所有寫操作。
| 1 | class HelperSingleton { |
| 2 | ????static Helper singleton = new Helper(); |
對32位的基本類型變量DCL是有效的
雖然DCL模式不能用于對象引用,但可以用于32位的基本類型變量。注意,DCL也不能用于對long和double類型的基本變量,因為不能保證未同步的64位基本變量的讀寫是原子操作。
| 01 | // Correct Double-Checked Locking for 32-bit primitives |
| 03 | ??private int cachedHashCode = 0; |
| 04 | ??public int hashCode() { |
| 05 | ????int h = cachedHashCode; |
| 07 | ????synchronized(this) { |
| 08 | ??????if (cachedHashCode != 0) return cachedHashCode; |
| 09 | ??????h = computeHashCode(); |
| 10 | ??????cachedHashCode = h; |
| 14 | ??// other functions and members... |
事實上,如果computeHashCode方法總是返回相同的結(jié)果且沒有其它附屬作用時(即,computeHashCode是個冪等方法),甚至可以消除這里的所有同步。
| 01 | // Lazy initialization 32-bit primitives |
| 02 | // Thread-safe if computeHashCode is idempotent |
| 04 | ??private int cachedHashCode = 0; |
| 05 | ??public int hashCode() { |
| 06 | ????int h = cachedHashCode; |
| 08 | ??????h = computeHashCode(); |
| 09 | ??????cachedHashCode = h; |
| 13 | ??// other functions and members... |
用顯式的內(nèi)存屏障使DCL有效
如果有顯式的內(nèi)存屏障指令可用,則有可能使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", |
| 05 | template <class TYPE, class LOCK> TYPE * |
| 06 | Singleton<TYPE, LOCK>::instance (void) { |
| 08 | ????TYPE* tmp = instance_; |
| 09 | ????// Insert the CPU-specific memory barrier instruction |
| 10 | ????// to synchronize the cache lines on multi-processor. |
| 11 | ????asm ("memoryBarrier"); |
| 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; |
用線程局部存儲來修復(fù)DCL
Alexander Terekhov (TEREKHOV@de.ibm.com)提出了個能實現(xiàn)DCL的巧妙的做法 —— 使用線程局部存儲。每個線程各自保存一個flag來表示該線程是否執(zhí)行了同步。
| 02 | ?/** If perThreadInstance.get() returns a non-null value, this thread |
| 03 | ????has done synchronization needed to see initialization |
| 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; |
| 11 | ?????private final void createHelper() { |
| 12 | ?????????synchronized(this) { |
| 13 | ?????????????if (helper == null) |
| 14 | ?????????????????helper = new Helper(); |
| 16 | ?????// Any non-null value would do as the argument here |
| 17 | ?????????perThreadInstance.set(perThreadInstance); |
這種方式的性能嚴重依賴于所使用的JDK實現(xiàn)。在Sun 1.2的實現(xiàn)中,ThreadLocal是非常慢的。在1.3中變得更快了,期望能在1.4上更上一個臺階。Doug Lea分析了一些延遲初始化技術(shù)實現(xiàn)的性能
在新的Java內(nèi)存模型下
JDK5使用了新的Java內(nèi)存模型和線程規(guī)范。
用volatile修復(fù)DCL
JDK5以及后續(xù)版本擴展了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 |
| 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(); |
| 12 | ????????return helper; |
不可變對象的DCL
如果Helper是個不可變對象,那么Helper中的所有字段都是final的,那么不使用volatile也能使DCL生效。主要是因為指向不可變對象的引用應(yīng)該表現(xiàn)出形如int和float一樣的行為;讀寫不可變對象的引用是原子操作。
文章轉(zhuǎn)自 并發(fā)編程網(wǎng)-ifeve.com
總結(jié)
以上是生活随笔為你收集整理的有关“双重检查锁定失效”的说明的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。