在之前學(xué)習(xí)了單例模式在多線程下的設(shè)計(jì)恢恼,疑惑為何要加volatile關(guān)鍵字民傻。加與不加有什么區(qū)別呢?這里我們就來(lái)研究一下。單例模式的設(shè)計(jì)可以參考個(gè)人總結(jié)的這篇文章
??背景:在早期的JVM中漓踢,synchronized存在巨大的性能開(kāi)銷牵署。因此,有人想出了一個(gè)“聰明”的技巧:雙重檢查鎖定(Double-Checked Locking)喧半。人們想通過(guò)雙重檢查鎖定來(lái)降低同步的開(kāi)銷奴迅。下面是使用雙重檢查鎖定來(lái)實(shí)現(xiàn)延遲初始化的示例代碼。
public class DoubleCheckedLocking { // 1
private static Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次檢查
synchronized (DoubleCheckedLocking.class) { // 5:加鎖
if (instance == null) // 6:第二次檢查
instance = new Instance(); // 7:問(wèn)題的根源出在這里
} // 8
} // 9
return instance; // 10
} // 11
}
上述的Instance類變量是沒(méi)有用volatile關(guān)鍵字修飾的薯酝,會(huì)導(dǎo)致這樣一個(gè)問(wèn)題:
在線程執(zhí)行到第4行的時(shí)候半沽,代碼讀取到instance不為null時(shí),instance引用的對(duì)象有可能還沒(méi)有完成初始化吴菠。
主要的原因是重排序者填。重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重新排序的一種手段。
第7行的代碼創(chuàng)建了一個(gè)對(duì)象做葵,這一行代碼可以分解成3個(gè)操作:
memory = allocate(); // 1:分配對(duì)象的內(nèi)存空間
ctorInstance(memory); // 2:初始化對(duì)象
instance = memory; // 3:設(shè)置instance指向剛分配的內(nèi)存地址
根源在于代碼中的2和3之間占哟,可能會(huì)被重排序。例如:
memory = allocate(); // 1:分配對(duì)象的內(nèi)存空間
instance = memory; // 3:設(shè)置instance指向剛分配的內(nèi)存地址
// 注意酿矢,此時(shí)對(duì)象還沒(méi)有被初始化榨乎!
ctorInstance(memory); // 2:初始化對(duì)象
這在單線程環(huán)境下是沒(méi)有問(wèn)題的,但在多線程環(huán)境下會(huì)出現(xiàn)問(wèn)題:
B線程會(huì)看到一個(gè)還沒(méi)有被初始化的對(duì)象瘫筐。
A2和A3的重排序不影響線程A的最終結(jié)果蜜暑,但會(huì)導(dǎo)致線程B在B1處判斷出instance不為空,線程B接下來(lái)將訪問(wèn)instance引用的對(duì)象策肝。此時(shí)肛捍,線程B將會(huì)訪問(wèn)到一個(gè)還未初始化的對(duì)象。
所以只需要做一點(diǎn)小的修改(把instance聲明為volatile型)之众,就可以實(shí)現(xiàn)線程安全的延遲初始化拙毫。因?yàn)楸籿olatile關(guān)鍵字修飾的變量是被禁止重排序的。