作者寫的太明白了雳旅,忍不住轉(zhuǎn)載分享:https://www.cnblogs.com/xz816111/p/8470048.html
在實(shí)現(xiàn)單例模式時(shí),如果未考慮多線程的情況间聊,就容易寫出下面的錯(cuò)誤代碼:
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public static 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 statc 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)操作之前橘忱。
至此顷扩,雙重檢查鎖就可以完美工作了火邓。
再次對(duì)原作者致以敬意飞崖,原文地址:https://www.cnblogs.com/xz816111/p/8470048.html