摘要:雙重檢查鎖定以下稱為已被廣泛當(dāng)做多線程環(huán)境下延遲初始化的一種高效手段。由于沒有對這些做出明確規(guī)定,很難說是否有效。可以在中使用顯式的內(nèi)存屏障來使生效,但中并沒有這些屏障。如果改變鎖釋放的語義釋放時執(zhí)行一個雙向的內(nèi)存屏障將會帶來性能損失。
雙重檢查鎖定(以下稱為DCL)已被廣泛當(dāng)做多線程環(huán)境下延遲初始化的一種高效手段。
遺憾的是,在Java中,如果沒有額外的同步,它并不可靠。在其它語言中,如c++,實現(xiàn)DCL,需要依賴于處理器的內(nèi)存模型、編譯器實行的重排序以及編譯器與同步庫之間的交互。由于c++沒有對這些做出明確規(guī)定,很難說DCL是否有效。可以在c++中使用顯式的內(nèi)存屏障來使DCL生效,但Java中并沒有這些屏障。
// Single threaded version class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) helper = new Helper(); return helper; } // other functions and members... }
如果這段代碼用在多線程環(huán)境下,有幾個可能出錯的地方。最明顯的是,可能會創(chuàng)建出兩或多個Helper對象。(后面會提到其它問題)。將getHelper()方法改為同步即可修復(fù)此問題。
// Correct multithreaded version class Foo { private Helper helper = null; public synchronized Helper getHelper() { if (helper == null) helper = new Helper(); return helper; } // other functions and members... }
上面的代碼在每次調(diào)用getHelper時都會執(zhí)行同步操作。DCL模式旨在消除helper對象被創(chuàng)建后還需要的同步。
// Broken multithreaded version // "Double-Checked Locking" idiom class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) synchronized(this) { if (helper == null) helper = new Helper(); } return helper; } // other functions and members... }
不幸的是,這段代碼無論是在優(yōu)化型的編譯器下還是在共享內(nèi)存處理器中都不能有效工作。
不起作用上面代碼不起作用的原因有很多。接下來我們先說幾個比較顯而易見的原因。理解這些之后,也許你想找出一種方法來“修復(fù)”DCL模式。你的修復(fù)也不會起作用:這里面有很微妙的原因。在理解了這些原因之后,可能想進一步進行修復(fù),但仍不會正常工作,因為存在更微妙的原因。
很多聰明的人在這上面花費了很多時間。除了在每個線程訪問helper對象時執(zhí)行鎖操作別無他法。
不起作用的第一個原因最顯而易見的原因是,Helper對象初始化時的寫操作與寫入helper字段的操作可以是無序的。這樣的話,如果某個線程調(diào)用getHelper()可能看到helper字段指向了一個Helper對象,但看到該對象里的字段值卻是默認(rèn)值,而不是在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不能正常工作的例子。下面的代碼做了些許整理:
public class DoubleCheckTest { // static data to aid in creating N singletons static final Object dummyObject = new Object(); // for reference init static final int A_VALUE = 256; // value to initialize "a" to static final int B_VALUE = 512; // value to initialize "b" to static final int C_VALUE = 1024; static ObjectHolder[] singletons; // array of static references static Thread[] threads; // array of racing threads static int threadCount; // number of threads to create static int singletonCount; // number of singletons to create static volatile int recentSingleton; // I am going to set a couple of threads racing, // trying to create N singletons. Basically the // race is to initialize a single array of // singleton references. The threads will use // double checked locking to control who // initializes what. Any thread that does not // initialize a particular singleton will check // to see if it sees a partially initialized view. // To keep from getting accidental synchronization, // each singleton is stored in an ObjectHolder // and the ObjectHolder is used for // synchronization. In the end the structure // is not exactly a singleton, but should be a // close enough approximation. // // This class contains data and simulates a // singleton. The static reference is stored in // a static array in DoubleCheckFail. static class Singleton { public int a; public int b; public int c; public Object dummy; public Singleton() { a = A_VALUE; b = B_VALUE; c = C_VALUE; dummy = dummyObject; } } static void checkSingleton(Singleton s, int index) { int s_a = s.a; int s_b = s.b; int s_c = s.c; Object s_d = s.dummy; if(s_a != A_VALUE) System.out.println("[" + index + "] Singleton.a not initialized " + s_a); if(s_b != B_VALUE) System.out.println("[" + index + "] Singleton.b not intialized " + s_b); if(s_c != C_VALUE) System.out.println("[" + index + "] Singleton.c not intialized " + s_c); if(s_d != dummyObject) if(s_d == null) System.out.println("[" + index + "] Singleton.dummy not initialized," + " value is null"); else System.out.println("[" + index + "] Singleton.dummy not initialized," + " value is garbage"); } // Holder used for synchronization of // singleton initialization. static class ObjectHolder { public Singleton reference; } static class TestThread implements Runnable { public void run() { for(int i = 0; i < singletonCount; ++i) { ObjectHolder o = singletons[i]; if(o.reference == null) { synchronized(o) { if (o.reference == null) { o.reference = new Singleton(); recentSingleton = i; } // shouldn"t have to check singelton here // mutex should provide consistent view } } else { checkSingleton(o.reference, i); int j = recentSingleton-1; if (j > i) i = j; } } } } public static void main(String[] args) { if( args.length != 2 ) { System.err.println("usage: java DoubleCheckFail" + ""); } // read values from args threadCount = Integer.parseInt(args[0]); singletonCount = Integer.parseInt(args[1]); // create arrays threads = new Thread[threadCount]; singletons = new ObjectHolder[singletonCount]; // fill singleton array for(int i = 0; i < singletonCount; ++i) singletons[i] = new ObjectHolder(); // fill thread array for(int i = 0; i < threadCount; ++i) threads[i] = new Thread( new TestThread() ); // start threads for(int i = 0; i < threadCount; ++i) threads[i].start(); // wait for threads to finish for(int i = 0; i < threadCount; ++i) { try { System.out.println("waiting to join " + i); threads[i].join(); } catch(InterruptedException ex) { System.out.println("interrupted"); } } System.out.println("done"); } }
當(dāng)上述代碼運行在使用Symantec JIT的系統(tǒng)上時,不能正常工作。尤其是,Symantec
JIT將
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)存模型了))。
基于前文解釋的原因,一些人提出了下面的代碼:
// (Still) Broken multithreaded version // "Double-Checked Locking" idiom class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) { Helper h; synchronized(this) { h = helper; if (h == null) synchronized (this) { h = new Helper(); } // release inner synchronization lock helper = h; } } return helper; } // 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)存屏障 —— 將會帶來性能損失。
可以做些事情迫使寫操作的時候執(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)),帶來的影響更大。
如果你要創(chuàng)建的是static單例對象(即,只會創(chuàng)建一個Helper對象),這里有個簡單優(yōu)雅的解決方案。
只需將singleton變量作為另一個類的靜態(tài)字段。Java的語義保證該字段被引用前是不會被初始化的,且任一訪問該字段的線程都會看到由初始化該字段所引發(fā)的所有寫操作。
class HelperSingleton { static Helper singleton = new Helper(); }對32位的基本類型變量DCL是有效的
雖然DCL模式不能用于對象引用,但可以用于32位的基本類型變量。注意,DCL也不能用于對long和double類型的基本變量,因為不能保證未同步的64位基本變量的讀寫是原子操作。
// Correct Double-Checked Locking for 32-bit primitives class Foo { private int cachedHashCode = 0; public int hashCode() { int h = cachedHashCode; if (h == 0) synchronized(this) { if (cachedHashCode != 0) return cachedHashCode; h = computeHashCode(); cachedHashCode = h; } return h; } // other functions and members... }
事實上,如果computeHashCode方法總是返回相同的結(jié)果且沒有其它附屬作用時(即,computeHashCode是個冪等方法),甚至可以消除這里的所有同步。
// Lazy initialization 32-bit primitives // Thread-safe if computeHashCode is idempotent class Foo { private int cachedHashCode = 0; public int hashCode() { int h = cachedHashCode; if (h == 0) { h = computeHashCode(); cachedHashCode = h; } return h; } // other functions and members... }用顯式的內(nèi)存屏障使DCL有效
如果有顯式的內(nèi)存屏障指令可用,則有可能使DCL生效。例如,如果你用的是C++,可以參考來自Doug
Schmidt等人所著書中的代碼:
// C++ implementation with explicit memory barriers // Should work on any platform, including DEC Alphas // From "Patterns for Concurrent and Distributed Objects", // by Doug Schmidt template用線程局部存儲來修復(fù)DCLTYPE * Singleton ::instance (void) { // First check TYPE* tmp = instance_; // Insert the CPU-specific memory barrier instruction // to synchronize the cache lines on multi-processor. asm ("memoryBarrier"); if (tmp == 0) { // Ensure serialization (guard // constructor acquires lock_). Guard guard (lock_); // Double check. tmp = instance_; if (tmp == 0) { tmp = new TYPE; // Insert the CPU-specific memory barrier instruction // to synchronize the cache lines on multi-processor. asm ("memoryBarrier"); instance_ = tmp; } return tmp; }
Alexander Terekhov (TEREKHOV@de.ibm.com)提出了個能實現(xiàn)DCL的巧妙的做法 ——
使用線程局部存儲。每個線程各自保存一個flag來表示該線程是否執(zhí)行了同步。
class Foo { /** If perThreadInstance.get() returns a non-null value, this thread has done synchronization needed to see initialization of helper */ private final ThreadLocal perThreadInstance = new ThreadLocal(); private Helper helper = null; public Helper getHelper() { if (perThreadInstance.get() == null) createHelper(); return helper; } private final void createHelper() { synchronized(this) { if (helper == null) helper = new Helper(); } // Any non-null value would do as the argument here perThreadInstance.set(perThreadInstance); } }
這種方式的性能嚴(yán)重依賴于所使用的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ù)DCLJDK5以及后續(xù)版本擴展了volatile語義,不再允許volatile寫操作與其前面的讀寫操作重排序,也不允許volatile讀操作與其后面的讀寫操作重排序。更多詳細信息見Jeremy Manson的博客。
// Works with acquire/release semantics for volatile // Broken under current semantics for volatile class Foo { private volatile Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized(this) { if (helper == null) helper = new Helper(); } } return helper; } }不可變對象的DCL
如果Helper是個不可變對象,那么Helper中的所有字段都是final的,那么不使用volatile也能使DCL生效。主要是因為指向不可變對象的引用應(yīng)該表現(xiàn)出形如int和float一樣的行為;讀寫不可變對象的引用是原子操作。
原文 Double Checked Locking
翻譯 丁一
via ifeve
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/64060.html
摘要:注意,禁止指令重排序在之后才被修復(fù)使用局部變量優(yōu)化性能重新查看中雙重檢查鎖定代碼。幫助文檔雙重檢查鎖定與延遲初始化有關(guān)雙重檢查鎖定失效的說明 雙重檢查鎖定(Double check locked)模式經(jīng)常會出現(xiàn)在一些框架源碼中,目的是為了延遲初始化變量。這個模式還可以用來創(chuàng)建單例。下面來看一個 Spring 中雙重檢查鎖定的例子。 showImg(https://segmentfaul...
摘要:非線程安全的雙重檢查鎖這里看起來很完美,但是是一個錯誤的優(yōu)化,代碼在讀取到不為的時候,引用的對象有可能換沒有完成初始化,這樣返回的是有問題的。 在Java多線程程序中,有時需要采用延遲初始化來降低初始化類和創(chuàng)建對象的開銷,雙重檢查鎖定是常見的延遲初始化技術(shù),但它是一種錯誤的用法 雙重檢查鎖的演進以及問題 使用syncronized實現(xiàn) public synchronized stati...
摘要:基于的雙重檢查鎖定的解決方案對于前面的基于雙重檢查鎖定來實現(xiàn)延遲初始化的方案指示例代碼,我們只需要做一點小的修改把聲明為型,就可以實現(xiàn)線程安全的延遲初始化。 雙重檢查鎖定的由來 在java程序中,有時候可能需要推遲一些高開銷的對象初始化操作,并且只有在使用這些對象時才進行初始化。此時程序員可能會采用延遲初始化。但要正確實現(xiàn)線程安全的延遲初始化需要一些技巧,否則很容易出現(xiàn)問題。比如,下...
摘要:對于而言,它執(zhí)行的是一個個指令。在指令中創(chuàng)建對象和賦值操作是分開進行的,也就是說語句是分兩步執(zhí)行的。此時線程打算使用實例,卻發(fā)現(xiàn)它沒有被初始化,于是錯誤發(fā)生了。 1.餓漢式單例 public class Singleton { private static Singleton instance = new Singleton(); ...
閱讀 3222·2021-11-11 16:55
閱讀 2458·2021-10-13 09:39
閱讀 2392·2021-09-13 10:27
閱讀 2155·2019-08-30 15:55
閱讀 3083·2019-08-30 15:54
閱讀 3127·2019-08-29 16:34
閱讀 1819·2019-08-29 12:41
閱讀 1065·2019-08-29 11:33