在這本書(shū)中文版的第219頁(yè)有個(gè)例子,講lazy load時(shí)用到double check,double check比直接用同步的好處是,當(dāng)Singleton初始化后,就不會(huì)有額外的同步操作。它的例子是

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

不幸的是,雙重檢查不會(huì)保證正常工作,因?yàn)榫幾g器會(huì)在Singleton的構(gòu)造方法被調(diào)用之前隨意給INSTANCE先付一個(gè)值。如果在INSTANCE引用被賦值之后而被初始化之前線程1被切換,線程2就會(huì)被返回一個(gè)對(duì)未初始化完全的單例類實(shí)例的引用。這樣在程序的其他方法中使用時(shí)可能會(huì)出現(xiàn)未知的錯(cuò)誤。
個(gè)人一開(kāi)始認(rèn)為正確的寫法,應(yīng)該是這樣的

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

利用一個(gè)tempInstance局部變量來(lái)排除返回實(shí)例未初始化完全的情況。因?yàn)槊看闻袛嗟亩际蔷植孔兞浚總€(gè)線程都會(huì)有一個(gè)自己的tempInstance,這樣就保證每個(gè)線程的tempInstance要么是初始化完全的要么就是未初始化的,不會(huì)出現(xiàn)中間的情況。要注意的是SingletonNew的(1)處是不能去掉的,比如線程構(gòu)造了一個(gè)實(shí)例,線程2此時(shí)等待在那里,線程2得到鎖,判斷tempInstance == null結(jié)果是true,又初始化了一次,這就不是單例了。(2)處的賦值順序也是不能顛倒的,如果顛倒就會(huì)出現(xiàn)和Singleton類一樣的情形。
請(qǐng)大家詳細(xì)討論,詳細(xì)解釋一下。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
其實(shí)這兩種寫法在舊的JMM上都是錯(cuò)誤的,在新的JMM上都是對(duì)的,錯(cuò)誤的原因主要是JMM對(duì)代碼的重新排序和優(yōu)化,新的JMM又對(duì)volatile的語(yǔ)義進(jìn)行了擴(kuò)展,保證了double-check的正確性。很抱歉一開(kāi)始讓一些博友產(chǎn)生了困惑,謝謝大家的熱心的討論和回帖,我的主要問(wèn)題就是出現(xiàn)在對(duì)JMM了解不夠深入,只是碎片式的了解一些,沒(méi)有很好的了解編譯器對(duì)代碼的重新排序和優(yōu)化,當(dāng)然編譯原理課上是學(xué)過(guò)的。二又沒(méi)有很好的掌握到volatile的新的語(yǔ)義。其實(shí)對(duì)一些細(xì)節(jié)了解清楚,可以避免我們的代碼出現(xiàn)一些奇怪的問(wèn)題,特別是在多線程環(huán)境中。
Jvm編譯器會(huì)對(duì)生成的代碼進(jìn)行優(yōu)化,重新排序,甚至移除它認(rèn)為不必要的代碼,volatile變量之間也是沒(méi)有順序保證的。然而jvm保證了classloader load字節(jié)碼和靜態(tài)變量初始化的同步性,所有把singleton設(shè)置為靜態(tài)變量是沒(méi)有問(wèn)題的。JMM保證了單線程執(zhí)行的效果和程序的順序是相同的。JVM對(duì)代碼的重新排序和優(yōu)化是對(duì)于程序不可見(jiàn)的,所以在例子2中我不應(yīng)該假設(shè)執(zhí)行的順序。在讀volatile變量之前,寫行為確保執(zhí)行完畢,并且更新的值會(huì)從線程工作內(nèi)存(CPU緩存,寄存器)刷新到主內(nèi)存中,JMM禁止volatile讀入寄存器,其他線程讀取時(shí)也會(huì)重新load到工作內(nèi)存中,保證了一致性和可見(jiàn)性,避免讀取臟數(shù)據(jù)。以前一直以為volatile涉及的只是變量可見(jiàn)性問(wèn)題,或者說(shuō)對(duì)可見(jiàn)性的適用范圍沒(méi)有很好的理解,并不涉及JMM順序性和原子性問(wèn)題。新的JMM對(duì)它進(jìn)行了擴(kuò)展,它對(duì)volatile變量的重新排序也做了限制。在舊的內(nèi)存模型當(dāng)中,volatile變量的多次訪問(wèn)之間是不能重新排序的,但是它們能在和對(duì)非volatile變量訪問(wèn)代碼之間進(jìn)行重新排序,新的內(nèi)存模型不同的是,volatile訪問(wèn)行為在和非volatile變量的訪問(wèn)行為的代碼之間重新排序加了一些限制。對(duì)volatile的寫行為就和synchronize方法或block釋放監(jiān)視器(鎖)的效果是一樣的,對(duì)volatile字段的讀操作和監(jiān)視器(鎖)的申請(qǐng)效果也是一樣的。新的模型在volatile字段訪問(wèn)上做了一些嚴(yán)格的限制,只對(duì)當(dāng)前線程可見(jiàn)的變量寫入到volatile共享變量f后,當(dāng)其他線程讀取f后就是可見(jiàn)的。
下面這個(gè)簡(jiǎn)單的例子:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//uses x - guaranteed to see 42.
}
}
}
假設(shè)當(dāng)前一個(gè)線程正在調(diào)用writer方法,其他線程正在調(diào)用reader方法,writer方法中對(duì)v的寫行為將對(duì)x的寫行為釋放到了內(nèi)存中,v變量的讀取,又重新從內(nèi)存中獲取了新值。因此,如果讀方法看到了v的值被設(shè)為true,也保證了它在這之前就可以看到x的新值42,但這在舊的內(nèi)存模型中是不保證的。如果v不是volatile的,編譯器可能就會(huì)對(duì)writer和reader中的代碼進(jìn)行重新排序,reader方法的訪問(wèn)有可能得到的x就是0. 可見(jiàn)在新的JMM中,volatile的語(yǔ)義得到了很好的加強(qiáng),每次對(duì)volatile字段的讀和寫可看作是都是半同步。這種順序性(happen-before關(guān)系)是針對(duì)同一個(gè)volatile字段而言的,對(duì)不同volatile字段的讀取還是沒(méi)有這種順序保證的。在新的JMM下,用volatile就可以解決問(wèn)題,線程1實(shí)例的初始化和線程2的讀取volatile變量就存在一個(gè)happen-before關(guān)系。
JMM對(duì)順序性只是提出了一些規(guī)則,具體如何重新排序還是不得而知。
參考文章:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#reordering
《JAVA Language Specification》 17.4