DCL(單例雙重檢查鎖模式)詳解
先看一下DCL(雙重檢查鎖模式)的示例代碼:
public class Singleton {
//Singleton對(duì)象屬性,加上volatile關(guān)鍵字是為了防止指定重排序,要知道singleton = new Singleton()拆分成cpu指令的話,有足足3個(gè)步驟
private volatile static Singleton singleton;
//對(duì)外提供的獲取實(shí)例的方法
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}從代碼里可以看到,做了兩重的singleton == null的判斷,中間還用了synchronized關(guān)鍵字,第一個(gè)singleton == null的判斷是為了避免線程串行化,如果為空,就進(jìn)入synchronized代碼塊中,獲取鎖后再操作,如果不為空,直接就返回singleton對(duì)象了,無(wú)需再進(jìn)行鎖競(jìng)爭(zhēng)和等待了。而第二個(gè)singleton == null的判斷是為了防止有多個(gè)線程同時(shí)跳過(guò)第一個(gè)singleton == null的判斷,比如線程一先獲取到鎖,進(jìn)入同步代碼塊中,發(fā)現(xiàn)singleton實(shí)例還是null,就會(huì)做new操作,然后退出同步代碼塊并釋放鎖,這時(shí)一起跳過(guò)第一層singleton == null的判斷的還有線程二,這時(shí)線程一釋放了鎖,線程二就會(huì)獲取到鎖,如果沒有第二層的singleton == null這個(gè)判斷擋著,那就會(huì)再創(chuàng)建一個(gè)singleton實(shí)例,就違反了單例的約束了。
那為什么要加volatile關(guān)鍵字呢
了解下singleton = new Singleton()這段代碼其實(shí)不是原子性的操作,它至少分為以下3個(gè)步驟:
給singleton對(duì)象分配內(nèi)存空間
調(diào)用Singleton類的構(gòu)造函數(shù)等,初始化singleton對(duì)象
將singleton對(duì)象指向分配的內(nèi)存空間,這步一旦執(zhí)行了,那singleton對(duì)象就不等于null了
這里還需要知道一點(diǎn),就是有時(shí)候JVM會(huì)為了優(yōu)化,而做指令重排序的操作,這里的指令,指的是CPU層面的。
正常情況下,singleton = new Singleton()的步驟是按照1->2->3這種步驟進(jìn)行的,但是一旦JVM做了指令重排序,那么順序很可能編程1->3->2,如果是這種順序,可以發(fā)現(xiàn),在3步驟執(zhí)行完singleton對(duì)象就不等于null,但是它其實(shí)還沒做步驟二的初始化工作,但是另一個(gè)線程進(jìn)來(lái)時(shí)發(fā)現(xiàn),singleton不等于null了,就這樣把半成品的實(shí)例返回去,調(diào)用是會(huì)報(bào)錯(cuò)的。
可以畫個(gè)出現(xiàn)指令重排序的圖加深下理解:

出現(xiàn)了指令重排序后,按照上圖的流程邏輯,很可能會(huì)返回還沒完成初始化的singleton對(duì)象,導(dǎo)致使用這個(gè)對(duì)象時(shí)報(bào)錯(cuò),而volatile關(guān)鍵字的作用之一就是禁止指令重排序。
總結(jié)
DCL使用volatile關(guān)鍵字,是為了禁止指令重排序,避免返回還沒完成初始化的singleton對(duì)象,導(dǎo)致調(diào)用報(bào)錯(cuò),也保證了線程的安全。
先看一下DCL(雙重檢查鎖模式)的示例代碼:
public class Singleton {
//Singleton對(duì)象屬性,加上volatile關(guān)鍵字是為了防止指定重排序,要知道singleton = new Singleton()拆分成cpu指令的話,有足足3個(gè)步驟
private volatile static Singleton singleton;
//對(duì)外提供的獲取實(shí)例的方法
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
從代碼里可以看到,做了兩重的singleton == null的判斷,中間還用了synchronized關(guān)鍵字,第一個(gè)singleton == null的判斷是為了避免線程串行化,如果為空,就進(jìn)入synchronized代碼塊中,獲取鎖后再操作,如果不為空,直接就返回singleton對(duì)象了,無(wú)需再進(jìn)行鎖競(jìng)爭(zhēng)和等待了。而第二個(gè)singleton == null的判斷是為了防止有多個(gè)線程同時(shí)跳過(guò)第一個(gè)singleton == null的判斷,比如線程一先獲取到鎖,進(jìn)入同步代碼塊中,發(fā)現(xiàn)singleton實(shí)例還是null,就會(huì)做new操作,然后退出同步代碼塊并釋放鎖,這時(shí)一起跳過(guò)第一層singleton == null的判斷的還有線程二,這時(shí)線程一釋放了鎖,線程二就會(huì)獲取到鎖,如果沒有第二層的singleton == null這個(gè)判斷擋著,那就會(huì)再創(chuàng)建一個(gè)singleton實(shí)例,就違反了單例的約束了。
那為什么要加volatile關(guān)鍵字呢
了解下singleton = new Singleton()這段代碼其實(shí)不是原子性的操作,它至少分為以下3個(gè)步驟:
給singleton對(duì)象分配內(nèi)存空間
調(diào)用Singleton類的構(gòu)造函數(shù)等,初始化singleton對(duì)象
將singleton對(duì)象指向分配的內(nèi)存空間,這步一旦執(zhí)行了,那singleton對(duì)象就不等于null了
這里還需要知道一點(diǎn),就是有時(shí)候JVM會(huì)為了優(yōu)化,而做指令重排序的操作,這里的指令,指的是CPU層面的。
正常情況下,singleton = new Singleton()的步驟是按照1->2->3這種步驟進(jìn)行的,但是一旦JVM做了指令重排序,那么順序很可能編程1->3->2,如果是這種順序,可以發(fā)現(xiàn),在3步驟執(zhí)行完singleton對(duì)象就不等于null,但是它其實(shí)還沒做步驟二的初始化工作,但是另一個(gè)線程進(jìn)來(lái)時(shí)發(fā)現(xiàn),singleton不等于null了,就這樣把半成品的實(shí)例返回去,調(diào)用是會(huì)報(bào)錯(cuò)的。
可以畫個(gè)出現(xiàn)指令重排序的圖加深下理解:

出現(xiàn)了指令重排序后,按照上圖的流程邏輯,很可能會(huì)返回還沒完成初始化的singleton對(duì)象,導(dǎo)致使用這個(gè)對(duì)象時(shí)報(bào)錯(cuò),而volatile關(guān)鍵字的作用之一就是禁止指令重排序。
總結(jié)
DCL使用volatile關(guān)鍵字,是為了禁止指令重排序,避免返回還沒完成初始化的singleton對(duì)象,導(dǎo)致調(diào)用報(bào)錯(cuò),也保證了線程的安全。
