前言
單例模式本身是很簡(jiǎn)單的,但是考慮到線程安全問題湾笛,簡(jiǎn)單的問題就變復(fù)雜了俯树。這里講解單例模式的雙重檢查。
單例模式演變
沒有多線程的世界
最開始的單例模式應(yīng)該是如下代碼畜挨。
public class Singleton {
private static Singleton singleton = null;
public static Singleton getSingleton() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
然后有人說你這個(gè)是線程不安全的筒繁。為什么是線程不安全噩凹?舉個(gè)例子。
A線程由于第一次進(jìn)來由于 singleton
實(shí)例為空毡咏,所以執(zhí)行new Singleton()
驮宴,然后將singleton
返回。由于多級(jí)緩存的存在呕缭,這個(gè)實(shí)例有可能只存在A線程的工作內(nèi)存中堵泽,并沒用立即往主內(nèi)存中寫。這個(gè)時(shí)候B線程進(jìn)來一樣判斷singleton == null
恢总,B線程取singleton
有兩個(gè)地方一個(gè)是自己的工作內(nèi)存迎罗,另一個(gè)是主內(nèi)存,但是都沒有片仿,因此B線程也要?jiǎng)?chuàng)建一遍singleton
纹安。
工作內(nèi)存與主內(nèi)存數(shù)據(jù)同步
這個(gè)時(shí)候就有人說了程癌,我們?cè)?code>singleton加上volatile
關(guān)鍵字赢笨,這樣只要有線程創(chuàng)建singleton
,都會(huì)往主內(nèi)存中寫楚堤,并且所有的線程都是可見的阳距。因此就有如下代碼塔粒。
public class Singleton {
// 加入volatile,所有線程都從主內(nèi)存中取值娄涩。
private static volatile Singleton singleton = null;
public static Singleton getSingleton() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
但是還是有線程安全問題窗怒,比如第一個(gè)線程在創(chuàng)建singleton
實(shí)例(還沒有創(chuàng)建完成)映跟,第二個(gè)線程判斷singleton==null
為true蓄拣,所以依然會(huì)創(chuàng)建兩次singleton
。這個(gè)時(shí)候就會(huì)想努隙,直接來個(gè)簡(jiǎn)單粗暴的方法球恤,直接在方法上加synchronized
。
簡(jiǎn)單粗暴的方法
public class Singleton {
private static Singleton singleton = null;
public static synchronized Singleton getSingleton() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
這下總算是滿足既要單例荸镊,又要線程安全咽斧。當(dāng)一直出現(xiàn)問題的時(shí)候,人們就想著趕快給我簡(jiǎn)單粗暴的解決方案吧躬存,只要解決問題就行张惹,不管什么方法都o(jì)k。當(dāng)問題得到解決的時(shí)候岭洲,程序也運(yùn)行了一段時(shí)間宛逗,人們又想著鎖住整個(gè)方法,這也太菜了盾剩,我得想著優(yōu)化方法雷激。鎖住部分代碼替蔬。
在優(yōu)化的路上漸行漸遠(yuǎn)
但是這也會(huì)出現(xiàn)線程安全問題,如下代碼線程A進(jìn)入創(chuàng)建singleton
屎暇,趁著線程A還沒有完全創(chuàng)建singleton
承桥,線程B登場(chǎng),由于A還在臨界區(qū)內(nèi)根悼,B線程只能等待凶异,接著A線程執(zhí)行完,線程B進(jìn)入臨界區(qū)創(chuàng)建singleton
挤巡,兩次創(chuàng)建singleton
唠帝,所以線程不安全。
public class Singleton {
private static Singleton singleton = null;
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
public class Singleton {
private static volatile Singleton singleton = null;
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
上面兩個(gè)線程不安全是因?yàn)榈诙€(gè)線程會(huì)進(jìn)入臨界區(qū)玄柏,并且創(chuàng)建singleton
襟衰,所以就有人提出在臨近區(qū)也對(duì)singleton
進(jìn)行判空。如下代碼粪摘,第二個(gè)線程進(jìn)入臨界區(qū)的時(shí)候判斷singleton == null
瀑晒,由于第一個(gè)線程已經(jīng)創(chuàng)建了singleton
并且釋放鎖了,JMM
會(huì)將線程A的singleton
對(duì)象刷新到主內(nèi)存中徘意,因此第二個(gè)線程從主內(nèi)存中獲取到singletone
苔悦,所以不會(huì)再次創(chuàng)建singleton
實(shí)例。乍一想好像沒有什么問題椎咧,但是以下代碼還是會(huì)存在線程安全問題玖详。這是為何?
public class Singleton {
private static Singleton singleton = null;
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
好生厲害的重排序
這就要牽扯到重新排序問題勤讽。為了提高運(yùn)行速度一般有以下重排序:
- 編譯器優(yōu)化重排序
- 指令級(jí)并行重排序
- 內(nèi)存系統(tǒng)重排序
singleton = new Singleton()
看上去是一行代碼蟋座,其實(shí)編譯后對(duì)應(yīng)的多條字節(jié)碼指令,一條字節(jié)碼指令可能轉(zhuǎn)換多條機(jī)器指令脚牍∠蛲危可以簡(jiǎn)單的把創(chuàng)建對(duì)象的過程看做以下步驟
1. 分配對(duì)象內(nèi)存空間
2. 初始化構(gòu)造函數(shù)
3. 賦值
按照順序執(zhí)行并沒有什么毛病,但是如果出現(xiàn)重排序?qū)?code>123變成132
那么對(duì)象沒有初始化完就被第二個(gè)線程看到诸狭,所以上述的代碼存在的問題是第一個(gè)線程執(zhí)行了13
過程券膀,第二個(gè)線程執(zhí)行到singleton == null
這個(gè)時(shí)候由于singleton
引用已經(jīng)指向內(nèi)存中的某塊區(qū)域,所以singleton== null
判斷就為false
驯遇,接下來第二個(gè)線程使用沒有初始化完成的對(duì)象芹彬。這個(gè)線程安全問題是重排序?qū)е碌模虼酥灰鉀Q重排序問題叉庐,就解決了該代碼的線程安全問題舒帮,這個(gè)時(shí)候就該引入volatile
,被volatile
的字段寫入的時(shí)候,JMM
會(huì)在寫入前插入一個(gè)StoreStore屏障会前,意思就是寫volatile
字段之前好乐,前面的寫操作都已完成,在這里這個(gè)屏障就是插入在23
之間瓦宜,因此singleton
被volatile
修飾后蔚万,指令執(zhí)行3
操作前12
操作都已完成。所以其他線程只要看到了singleton
對(duì)象临庇,那么該對(duì)象就一定初始化完成反璃,因此這就解決了線程安全問題。
public class Singleton {
private static volatile Singleton singleton = null;
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
總結(jié)
這里簡(jiǎn)單總結(jié)下單例模式的線程安全問題假夺。之所以出現(xiàn)安全問題有如下原因:
- 多級(jí)緩存與主內(nèi)存的值不同步淮蜈。
- 重排序問題。
- 對(duì)臨界區(qū)的訪問問題已卷。
因此只要解決上述三個(gè)問題梧田,基本上就解決單例模式的線程安全問題。