Java中的雙重檢查鎖(double checked locking)
借鑒該文章玻淑,表示感謝!
因?yàn)榕潞罄m(xù)找不到,所以自己再記錄了一遍
在實(shí)現(xiàn)單例模式時(shí)木羹,如果未考慮多線程的情況东囚,就容易寫出下面的錯(cuò)誤代碼:
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public Singleton getInstance() {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
return uniqueSingleton;
}
}
在多線程的情況下牌芋,這樣寫可能會(huì)導(dǎo)致uniqueSingleton有多個(gè)實(shí)例迂猴。比如下面這種情況佑菩,考慮有兩個(gè)線程同時(shí)調(diào)用getInstance():
Time | Thread A | Thread B |
---|---|---|
T1 | 檢查到uniqueSingleton為空 | |
T2 | 檢查到uniqueSingleton為空 | |
T3 | 初始化對(duì)象A | |
T4 | 返回對(duì)象A | |
T5 | 初始化對(duì)象B | |
T6 | 返回對(duì)象B |
可以看到扁凛,uniqueSingleton被實(shí)例化了兩次并且被不同對(duì)象持有忍疾。完全違背了單例的初衷。
加鎖
出現(xiàn)這種情況谨朝,第一反應(yīng)就是加鎖卤妒,如下:
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public synchronized Singleton getInstance() {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
return uniqueSingleton;
}
}
這樣雖然解決了問題,但是因?yàn)橛玫搅藄ynchronized,會(huì)導(dǎo)致很大的性能開銷,并且加鎖其實(shí)只需要在第一次初始化的時(shí)候用到听皿,之后的調(diào)用都沒必要再進(jìn)行加鎖。
雙重檢查鎖
雙重檢查鎖(double checked locking)是對(duì)上述問題的一種優(yōu)化士复。先判斷對(duì)象是否已經(jīng)被初始化,再?zèng)Q定要不要加鎖翩活。
錯(cuò)誤的雙重檢查鎖
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton(); // error
}
}
}
return uniqueSingleton;
}
}
如果這樣寫阱洪,運(yùn)行順序就成了:
檢查變量是否被初始化(不去獲得鎖),如果已被初始化則立即返回菠镇。
獲取鎖澄峰。
再次檢查變量是否已經(jīng)被初始化,如果還沒被初始化就初始化一個(gè)對(duì)象辟犀。
執(zhí)行雙重檢查是因?yàn)榍尉海绻鄠€(gè)線程同時(shí)了通過了第一次檢查绸硕,并且其中一個(gè)線程首先通過了第二次檢查并實(shí)例化了對(duì)象,那么剩余通過了第一次檢查的線程就不會(huì)再去實(shí)例化對(duì)象魂毁。
這樣玻佩,除了初始化的時(shí)候會(huì)出現(xiàn)加鎖的情況,后續(xù)的所有調(diào)用都會(huì)避免加鎖而直接返回席楚,解決了性能消耗的問題咬崔。
隱患
上述寫法看似解決了問題,但是有個(gè)很大的隱患烦秩。實(shí)例化對(duì)象的那行代碼(標(biāo)記為error的那行)垮斯,實(shí)際上可以分解成以下三個(gè)步驟:
分配內(nèi)存空間
初始化對(duì)象
將對(duì)象指向剛分配的內(nèi)存空間
但是有些編譯器為了性能的原因,可能會(huì)將第二步和第三步進(jìn)行重排序只祠,順序就成了:
分配內(nèi)存空間
將對(duì)象指向剛分配的內(nèi)存空間
初始化對(duì)象
現(xiàn)在考慮重排序后兜蠕,兩個(gè)線程發(fā)生了以下調(diào)用:
Time | Thread A | Thread B |
---|---|---|
T1 | 檢查到uniqueSingleton為空 | |
T2 | 獲取鎖 | |
T3 | 再次檢查到uniqueSingleton為空 | |
T4 | 為uniqueSingleton分配內(nèi)存空間 | |
T5 | 將uniqueSingleton指向內(nèi)存空間 | |
T6 | 檢查到uniqueSingleton不為空 | |
T7 | 訪問uniqueSingleton(此時(shí)對(duì)象還未完成初始化) | |
T8 | 初始化uniqueSingleton |
在這種情況下,T7時(shí)刻線程B對(duì)uniqueSingleton
的訪問抛寝,訪問的是一個(gè)初始化未完成的對(duì)象熊杨。
正確的雙重檢查鎖
public class Singleton {
private volatile static Singleton uniqueSingleton;
private Singleton() {
}
public Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
}
}
return uniqueSingleton;
}
}
為了解決上述問題,需要在uniqueSingleton
前加入關(guān)鍵字volatile
盗舰。使用了volatile關(guān)鍵字后晶府,重排序被禁止,所有的寫(write)操作都將發(fā)生在讀(read)操作之前钻趋。
至此川陆,雙重檢查鎖就可以完美工作了。
參考資料: