??摘要:CPU為了對(duì)程序進(jìn)行優(yōu)化,會(huì)對(duì)程序的指令進(jìn)行重排序,此時(shí)程序的執(zhí)行順序和代碼的編寫順序不一定一致,這就可能會(huì)引起有序性問題。

 

本文分享自華為云社區(qū)??《【高并發(fā)】解密導(dǎo)致并發(fā)問題的第三個(gè)幕后黑手——有序性問題》??,作者:冰 河 。 

有序性

有序性是指:按照代碼的既定順序執(zhí)行。


說的通俗一點(diǎn),就是代碼會(huì)按照指定的順序執(zhí)行,例如,按照程序編寫的順序執(zhí)行,先執(zhí)行第一行代碼,再執(zhí)行第二行代碼,然后是第三行代碼,以此類推。如下圖所示。


并發(fā)高?可能是編譯優(yōu)化引發(fā)有序性問題_有序性

指令重排序

編譯器或者解釋器為了優(yōu)化程序的執(zhí)行性能,有時(shí)會(huì)改變程序的執(zhí)行順序。但是,編譯器或者解釋器對(duì)程序的執(zhí)行順序進(jìn)行修改,可能會(huì)導(dǎo)致意想不到的問題!


在單線程下,指令重排序可以保證最終執(zhí)行的結(jié)果與程序順序執(zhí)行的結(jié)果一致,但是在多線程下就會(huì)存在問題。

如果發(fā)生了指令重排序,則程序可能先執(zhí)行第一行代碼,再執(zhí)行第三行代碼,然后執(zhí)行第二行代碼,如下所示。


并發(fā)高?可能是編譯優(yōu)化引發(fā)有序性問題_有序性_02


例如下面的三行代碼。


int x = 1; 
int y = 2;
int z = x + y;


CPU發(fā)生指令重排序時(shí),能夠保證x=1和y = 2這兩行代碼在z = x+ y這行代碼的上面,而x = 1和 y = 2的順序就不一定了。在單線程下不會(huì)出現(xiàn)問題,但是在多線程下就不一定了。

有序性問題

CPU為了對(duì)程序進(jìn)行優(yōu)化,會(huì)對(duì)程序的指令進(jìn)行重排序,此時(shí)程序的執(zhí)行順序和代碼的編寫順序不一定一致,這就可能會(huì)引起有序性問題。


在Java程序中,一個(gè)經(jīng)典的案例就是使用雙重檢查機(jī)制來創(chuàng)建單例對(duì)象。例如,在下面的代碼中,在getInstance()方法中獲取對(duì)象實(shí)例時(shí),首先判斷instance對(duì)象是否為空,如果為空,則鎖定當(dāng)前類的class對(duì)象,并再次檢查instance是否為空,如果instance對(duì)象仍然為空,則為instance對(duì)象創(chuàng)建一個(gè)實(shí)例。


package io.binghe.concurrent.lab01;

/**
* @author binghe
* @version 1.0.0
* @description 測(cè)試單例
*/
public class SingleInstance {

private static SingleInstance instance;

public static SingleInstance getInstance(){
if(instance == null){
synchronized (SingleInstance.class){
if(instance == null){
instance = new SingleInstance();
}
}
}
return instance;
}
}


如果編譯器或者解釋器不會(huì)對(duì)上面的程序進(jìn)行優(yōu)化,整個(gè)代碼的執(zhí)行過程如下所示。


并發(fā)高?可能是編譯優(yōu)化引發(fā)有序性問題_有序性_03


注意:為了讓大家更加明確流程圖的執(zhí)行順序,我在上圖中標(biāo)注了數(shù)字,以明確線程A和線程B執(zhí)行的順序。

?

假設(shè)此時(shí)有線程A和線程B兩個(gè)線程同時(shí)調(diào)用getInstance()方法來獲取對(duì)象實(shí)例,兩個(gè)線程會(huì)同時(shí)發(fā)現(xiàn)instance對(duì)象為空,此時(shí)會(huì)同時(shí)對(duì)SingleInstance.class加鎖,而JVM會(huì)保證只有一個(gè)線程獲取到鎖,這里我們假設(shè)是線程A獲取到鎖。則線程B由于未獲取到鎖而進(jìn)行等待。接下來,線程A再次判斷instance對(duì)象為空,從而創(chuàng)建instance對(duì)象的實(shí)例,最后釋放鎖。此時(shí),線程B被喚醒,線程B再次嘗試獲取鎖,獲取鎖成功后,線程B檢查此時(shí)的instance對(duì)象已經(jīng)不再為空,線程B不再創(chuàng)建instance對(duì)象。


上面的一切看起來很完美,但是這一切的前提是編譯器或者解釋器沒有對(duì)程序進(jìn)行優(yōu)化,也就是說CPU沒有對(duì)程序進(jìn)行重排序。而實(shí)際上,這一切都只是我們自己覺得是這樣的。


在真正高并發(fā)環(huán)境下運(yùn)行上面的代碼獲取instance對(duì)象時(shí),創(chuàng)建對(duì)象的new操作會(huì)因?yàn)榫幾g器或者解釋器對(duì)程序的優(yōu)化而出現(xiàn)問題。也就是說,問題的根源在于如下一行代碼。


instance = new SingleInstance();


對(duì)于上面的一行代碼來說,會(huì)有3個(gè)CPU指令與其對(duì)應(yīng)。

1.分配內(nèi)存空間。

2.初始化對(duì)象。

3.將instance引用指向內(nèi)存空間。


正常執(zhí)行的CPU指令順序?yàn)?—>2—>3,CPU對(duì)程序進(jìn)行重排序后的執(zhí)行順序可能為1—>3—>2。此時(shí),就會(huì)出現(xiàn)問題。


當(dāng)CPU對(duì)程序進(jìn)行重排序后的執(zhí)行順序?yàn)?—>3—>2時(shí),我們將線程A和線程B調(diào)用getInstance()方法獲取對(duì)象實(shí)例的兩種步驟總結(jié)如下所示。


【第一種步驟】

(1)假設(shè)線程A和線程B同時(shí)進(jìn)入第一個(gè)if條件判斷。

(2)假設(shè)線程A首先獲取到synchronized鎖,進(jìn)入synchronized代碼塊,此時(shí)因?yàn)閕nstance對(duì)象為null,所以,此時(shí)執(zhí)行instance = new SingleInstance()語句。

(3)在執(zhí)行instance = new SingleInstance()語句時(shí),線程A會(huì)在JVM中開辟一塊空白的內(nèi)存空間。

(4)線程A將instance引用指向空白的內(nèi)存空間,在沒有進(jìn)行對(duì)象初始化的時(shí)候,發(fā)生了線程切換,線程A釋放synchronized鎖,CPU切換到線程B上。

(5)線程B進(jìn)入synchronized代碼塊,讀取到線程A返回的instance對(duì)象,此時(shí)這個(gè)instance不為null,但是并未進(jìn)行對(duì)象的初始化操作,是一個(gè)空對(duì)象。此時(shí),線程B如果使用instance,就可能出現(xiàn)問題?。?!


【第二種步驟】

(1)線程A先進(jìn)入if條件判斷,

(2)線程A獲取synchronized鎖,并進(jìn)行第二次if條件判斷,此時(shí)的instance為null,執(zhí)行instance = new SingleInstance()語句。

(3)線程A在JVM中開辟一塊空白的內(nèi)存空間。

(4)線程A將instance引用指向空白的內(nèi)存空間,在沒有進(jìn)行對(duì)象初始化的時(shí)候,發(fā)生了線程切換,CPU切換到線程B上。

(5)線程B進(jìn)行第一次if判斷,發(fā)現(xiàn)instance對(duì)象不為null,但是此時(shí)的instance對(duì)象并未進(jìn)行初始化操作,是一個(gè)空對(duì)象。如果線程B直接使用這個(gè)instance對(duì)象,就可能出現(xiàn)問題?。?!


在第二種步驟中,即使發(fā)生線程切換時(shí),線程A沒有釋放鎖,則線程B進(jìn)行第一次if判斷時(shí),發(fā)現(xiàn)instance已經(jīng)不為null,直接返回instance,而無需嘗試獲取synchronized鎖。

?

我們可以將上述過程簡化成下圖所示。


并發(fā)高?可能是編譯優(yōu)化引發(fā)有序性問題_內(nèi)存空間_04

總結(jié)

導(dǎo)致并發(fā)編程產(chǎn)生各種詭異問題的根源有三個(gè):緩存導(dǎo)致的可見性問題、線程切換導(dǎo)致的原子性問題和編譯優(yōu)化帶來的有序性問題。我們從根源上理解了這三個(gè)問題產(chǎn)生的原因,能夠幫助我們更好的編寫高并發(fā)程序。


??點(diǎn)擊關(guān)注,第一時(shí)間了解華為云新鮮技術(shù)~??