防重排序
我們從一個(gè)最經(jīng)典的例子來分析重排序問題。大家應(yīng)該都很熟悉單例模式的實(shí)現(xiàn),而在并發(fā)環(huán)境下的單例實(shí)現(xiàn)方式,我們通??梢圆捎秒p重檢查加鎖(DCL)的方式來實(shí)現(xiàn)。
其源碼如下:
現(xiàn)在我們分析一下為什么要在變量singleton之間加上volatile關(guān)鍵字。要理解這個(gè)問題,先要了解對象的構(gòu)造過程,實(shí)例化一個(gè)對象其實(shí)可以分為三個(gè)步驟:
分配內(nèi)存空間。 初始化對象。 將內(nèi)存空間的地址賦值給對應(yīng)的引用。
但是由于操作系統(tǒng)可以對指令進(jìn)行重排序,所以上面的過程也可能會變成如下過程:
分配內(nèi)存空間。 將內(nèi)存空間的地址賦值給對應(yīng)的引用。 初始化對象
如果是這個(gè)流程,多線程環(huán)境下就可能將一個(gè)未初始化的對象引用暴露出來,從而導(dǎo)致不可預(yù)料的結(jié)果。因此,為了防止這個(gè)過程的重排序,我們需要將變量設(shè)置為volatile類型的變量。
實(shí)現(xiàn)可見性
可見性問題主要指一個(gè)線程修改了共享變量值,而另一個(gè)線程卻看不到。引起可見性問題的主要原因是每個(gè)線程擁有自己的一個(gè)高速緩存區(qū)——線程工作內(nèi)存。
volatile關(guān)鍵字能有效的解決這個(gè)問題,我們看下下面的例子,就可以知道其作用:
直觀上說,這段代碼的結(jié)果只可能有兩種:b=3;a=3 或 b=2;a=1。不過運(yùn)行上面的代碼(可能時(shí)間上要長一點(diǎn)),你會發(fā)現(xiàn)除了上兩種結(jié)果之外,還出現(xiàn)了另外兩種結(jié)果:
為什么會出現(xiàn)b=2;a=3和b=3;a=1這種結(jié)果呢? 正常情況下,如果先執(zhí)行change方法,再執(zhí)行print方法,輸出結(jié)果應(yīng)該為b=3;a=3。相反,如果先執(zhí)行的print方法,再執(zhí)行change方法,結(jié)果應(yīng)該是 b=2;a=1。那b=3;a=1的結(jié)果是怎么出來的? 原因就是個(gè)線程將值a=3修改后,但是對第二個(gè)線程是不可見的,所以才出現(xiàn)這一結(jié)果。如果將a和b都改成volatile類型的變量再執(zhí)行,則再也不會出現(xiàn)b=2;a=3和b=3;a=1的結(jié)果了。
保證原子性:單次讀/寫
volatile不能保證完全的原子性,只能保證單次的讀/寫操作具有原子性。