原子性
原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。
一個(gè)很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問題:
比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個(gè)操作:從賬戶A減去1000元,往賬戶B加上1000元。
試想一下,如果這2個(gè)操作不具備原子性,會造成什么樣的后果。假如從賬戶A減去1000元之后,操作突然中止。然后又從B取出了500元,取出500元之后,再執(zhí)行 往賬戶B加上1000元 的操作。這樣就會導(dǎo)致賬戶A雖然減去了1000元,但是賬戶B沒有收到這個(gè)轉(zhuǎn)過來的1000元。
所以這2個(gè)操作必須要具備原子性才能保證不出現(xiàn)一些意外的問題。
同樣地反映到并發(fā)編程中會出現(xiàn)什么結(jié)果呢?
舉個(gè)最簡單的例子,大家想一下假如為一個(gè)32位的變量賦值過程不具備原子性的話,會發(fā)生什么后果?
i = 9;
假若一個(gè)線程執(zhí)行到這個(gè)語句時(shí),我暫且假設(shè)為一個(gè)32位的變量賦值包括兩個(gè)過程:為低16位賦值,為高16位賦值。
那么就可能發(fā)生一種情況:當(dāng)將低16位數(shù)值寫入之后,突然被中斷,而此時(shí)又有一個(gè)線程去讀取i的值,那么讀取到的就是錯(cuò)誤的數(shù)據(jù)。
可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
舉個(gè)簡單的例子,看下面這段代碼:
//線程1執(zhí)行的代碼
int i = 0;
i = 10;
//線程2執(zhí)行的代碼
j = i;
假若執(zhí)行線程1的是CPU1,執(zhí)行線程2的是CPU2。由上面的分析可知,當(dāng)線程1執(zhí)行 i =10這句時(shí),會先把i的初始值加載到CPU1的高速緩存中,然后賦值為10,那么在CPU1的高速緩存當(dāng)中i的值變?yōu)?0了,卻沒有立即寫入到主存當(dāng)中。
此時(shí)線程2執(zhí)行 j = i,它會先去主存讀取i的值并加載到CPU2的緩存當(dāng)中,注意此時(shí)內(nèi)存當(dāng)中i的值還是0,那么就會使得j的值為0,而不是10。
這就是可見性問題,線程1對變量i修改了之后,線程2沒有立即看到線程1修改的值。
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。舉個(gè)簡單的例子,看下面這段代碼:
int i = 0;
boolean flag = false;
i = 1; //語句1
flag = true; //語句2
上面代碼定義了一個(gè)int型變量,定義了一個(gè)boolean類型變量,然后分別對兩個(gè)變量進(jìn)行賦值操作。從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執(zhí)行這段代碼的時(shí)候會保證語句1一定會在語句2前面執(zhí)行嗎?不一定,為什么呢?這里可能會發(fā)生指令重排序(Instruction Reorder)。
下面解釋一下什么是指令重排序,一般來說,處理器為了提高程序運(yùn)行效率,可能會對輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
比如上面的代碼中,語句1和語句2誰先執(zhí)行對最終的程序結(jié)果并沒有影響,那么就有可能在執(zhí)行過程中,語句2先執(zhí)行而語句1后執(zhí)行。
但是要注意,雖然處理器會對指令進(jìn)行重排序,但是它會保證程序最終結(jié)果會和代碼順序執(zhí)行結(jié)果相同,那么它靠什么保證的呢?再看下面一個(gè)例子:
上面代碼定義了一個(gè)int型變量,定義了一個(gè)boolean類型變量,然后分別對兩個(gè)變量進(jìn)行賦值操作。從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執(zhí)行這段代碼的時(shí)候會保證語句1一定會在語句2前面執(zhí)行嗎?不一定,為什么呢?這里可能會發(fā)生指令重排序(Instruction Reorder)。
下面解釋一下什么是指令重排序,一般來說,處理器為了提高程序運(yùn)行效率,可能會對輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
比如上面的代碼中,語句1和語句2誰先執(zhí)行對最終的程序結(jié)果并沒有影響,那么就有可能在執(zhí)行過程中,語句2先執(zhí)行而語句1后執(zhí)行。
但是要注意,雖然處理器會對指令進(jìn)行重排序,但是它會保證程序最終結(jié)果會和代碼順序執(zhí)行結(jié)果相同,那么它靠什么保證的呢?再看下面一個(gè)例子:
int a = 10; //語句1
int r = 2; //語句2
a = a + 3; //語句3
r = a*a; //語句4
這段代碼有4個(gè)語句,那么可能的一個(gè)執(zhí)行順序是:這段代碼有4個(gè)語句,那么可能的一個(gè)執(zhí)行順序是:
那么可不可能是這個(gè)執(zhí)行順序呢: 語句2 語句1 語句4 語句3
不可能,因?yàn)樘幚砥髟谶M(jìn)行重排序時(shí)是會考慮指令之間的數(shù)據(jù)依賴性,如果一個(gè)指令I(lǐng)nstruction 2必須用到Instruction 1的結(jié)果,那么處理器會保證Instruction 1會在Instruction 2之前執(zhí)行。
雖然重排序不會影響單個(gè)線程內(nèi)程序執(zhí)行的結(jié)果,但是多線程呢?下面看一個(gè)例子:
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代碼中,由于語句1和語句2沒有數(shù)據(jù)依賴性,因此可能會被重排序。假如發(fā)生了重排序,在線程1執(zhí)行過程中先執(zhí)行語句2,而此是線程2會以為初始化工作已經(jīng)完成,那么就會跳出while循環(huán),去執(zhí)行doSomethingwithconfig(context)方法,而此時(shí)context并沒有被初始化,就會導(dǎo)致程序出錯(cuò)。
從上面可以看出,指令重排序不會影響單個(gè)線程的執(zhí)行,但是會影響到線程并發(fā)執(zhí)行的正確性。
也就是說,要想并發(fā)程序正確地執(zhí)行,必須要保證原子性、可見性以及有序性。只要有一個(gè)沒有被保證,就有可能會導(dǎo)致程序運(yùn)行不正確。