看似完美的單例模式?
public class Single {
private static Single3 instance;
private Single() {}
public static Single getInstance() {
if (instance == null) {
synchronized (Single.class) {
if (instance == null) {
instance = new Single3();
}
}
}
return instance;
}
}
- 第一個(gè)if (instance == null),其實(shí)是為了解決效率問(wèn)題缠沈,只有instance為null的時(shí)候,才進(jìn)入synchronized的代碼段——大大減少了幾率错蝴。
- 第二個(gè)if (instance == null)洲愤,則是為了防止可能出現(xiàn)多個(gè)實(shí)例的情況。
那么還會(huì)有問(wèn)題嗎顷锰?
答:只是『看起來(lái)』柬赐,還是有小概率出現(xiàn)問(wèn)題的。
原子操作
原子操作(atomic)就是不可分割的操作官紫,在計(jì)算機(jī)中肛宋,就是指不會(huì)因?yàn)?strong>線程調(diào)度被打斷的操作。
例子:
m = 6; // 這是個(gè)原子操作
假如m原先的值為0束世,那么對(duì)于這個(gè)操作酝陈,要么執(zhí)行成功m變成了6,要么是沒(méi)執(zhí)行m還是0毁涉,而不會(huì)出現(xiàn)諸如m=3這種中間態(tài)——即使是在并發(fā)的線程中沉帮。
而,聲明并賦值就不是一個(gè)原子操作:
int n = 6; // 這不是一個(gè)原子操作
對(duì)于這個(gè)語(yǔ)句贫堰,至少有兩個(gè)操作:
① 明一個(gè)變量n
② 給n賦值為6
——這樣就會(huì)有一個(gè)中間狀態(tài):變量n已經(jīng)被聲明了但是還沒(méi)有被賦值的狀態(tài)穆壕。
——這樣,在多線程中其屏,由于線程執(zhí)行順序的不確定性喇勋,如果兩個(gè)線程都使用m,就可能會(huì)導(dǎo)致不穩(wěn)定的結(jié)果出現(xiàn)偎行。
指令重排
概念
簡(jiǎn)單來(lái)說(shuō)川背,就是計(jì)算機(jī)為了提高執(zhí)行效率贰拿,會(huì)做的一些優(yōu)化,在不影響最終結(jié)果的情況下渗常,可能會(huì)對(duì)一些語(yǔ)句的執(zhí)行順序進(jìn)行調(diào)整。
例子:
int a ; // 語(yǔ)句1
a = 8 ; // 語(yǔ)句2
int b = 9 ; // 語(yǔ)句3
int c = a + b ; // 語(yǔ)句4
正常來(lái)說(shuō)皱碘,對(duì)于順序結(jié)構(gòu)询一,執(zhí)行的順序是自上到下,也即1234癌椿。
但是健蕊,由于指令重排的原因,因?yàn)?strong>不影響最終的結(jié)果踢俄,所以缩功,實(shí)際執(zhí)行的順序可能會(huì)變成3124或者1324。
由于語(yǔ)句3和4沒(méi)有原子性的問(wèn)題都办,語(yǔ)句3和語(yǔ)句4也可能會(huì)拆分成原子操作嫡锌,再重排。
——也就是說(shuō)琳钉,
對(duì)于非原子性的操作势木,在不影響最終結(jié)果的情況下,其拆分成的原子操作可能會(huì)被重新排列執(zhí)行順序歌懒。
回到話題
主要在于singleton = new Singleton()這句啦桌,這并非是一個(gè)原子操作,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情:
- 給 singleton 分配內(nèi)存
- 調(diào)用 Singleton 的構(gòu)造函數(shù)來(lái)初始化成員變量及皂,形成實(shí)例
- 將singleton對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 singleton才是非 null 了)
但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化甫男。也就是說(shuō)上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2验烧。如果是后者板驳,則在 3 執(zhí)行完畢、2 未執(zhí)行之前碍拆,被線程二搶占了笋庄,這時(shí) instance 已經(jīng)是非 null 了(但卻沒(méi)有初始化),所以線程二會(huì)直接返回 instance倔监,然后使用,然后順理成章地報(bào)錯(cuò)菌仁。
錯(cuò)誤根源:
再稍微解釋一下浩习,就是說(shuō),由于有一個(gè)『instance已經(jīng)不為null但是仍沒(méi)有完成初始化』的中間狀態(tài)济丘,而這個(gè)時(shí)候谱秽,如果有其他線程剛好運(yùn)行到第一層if (instance == null)這里洽蛀,這里讀取到的instance已經(jīng)不為null了,所以就直接把這個(gè)中間狀態(tài)的instance拿去用了疟赊,就會(huì)產(chǎn)生問(wèn)題郊供。這里的關(guān)鍵在于——線程T1對(duì)instance的寫操作沒(méi)有完成,線程T2就執(zhí)行了讀操作近哟。
完全版
public class Single {
private static volatile Single4 instance;
private Single() {}
public static Single getInstance() {
if (instance == null) {
synchronized (Single.class) {
if (instance == null) {
instance = new Single();
}
}
}
return instance;
}
}
volatile發(fā)揮的作用
volatile關(guān)鍵字的一個(gè)作用是禁止指令重排驮审,把instance聲明為volatile之后,對(duì)它的寫操作就會(huì)有一個(gè)內(nèi)存屏障(什么是內(nèi)存屏障吉执?)疯淫,這樣,在它的賦值完成之前戳玫,就不用會(huì)調(diào)用讀操作熙掺。
注意:volatile阻止的不是singleton = new Singleton()這句話內(nèi)部[1-2-3]的指令重排,而是保證了在一個(gè)寫操作([1-2-3])完成之前咕宿,不會(huì)調(diào)用讀操作(if (instance == null))币绩。