并發(fā)是讓多個(gè)線程同時(shí)履行,若線程之間是獨(dú)立的,那并發(fā)實(shí)現(xiàn)起來很簡(jiǎn)單,各自履行各自的就行;但常常多條線程之間需要同享數(shù)據(jù),此時(shí)在并發(fā)編程進(jìn)程中就不可避免要斟酌兩個(gè)問題:通訊 與 同步。
通訊
通訊是指消息在兩條線程之間傳遞。
既然要傳遞消息,那接收線程 和 發(fā)送線程之間必須要有個(gè)前后關(guān)系,此時(shí)就需要用到同步。通訊和同步是相輔相成的。
同步
同步是指,控制多條線程之間的履行次序。
線程之間的通訊1共有兩種方式:同享內(nèi)存 和 消息傳遞。
這類方式并沒有真正地實(shí)現(xiàn)消息傳遞,只是從結(jié)果上來看就像是將消息從1條線程傳遞到了另外一條線程。
綜上所述:對(duì)同享內(nèi)存的通訊方式,需要進(jìn)行顯示的同步,隱式的通訊;
而對(duì)消息傳遞的通訊方式,需要隱式的同步,顯示的通訊。
Java使用同享內(nèi)存的方式實(shí)現(xiàn)多線程之間的消息傳遞。因此,程序員需要寫額外的代碼用于線程之間的同步。
PS:其實(shí)同享內(nèi)存的方式從實(shí)現(xiàn)進(jìn)程來看,跟消息傳遞1點(diǎn)關(guān)系都沒有:1條線程將消息存入同享內(nèi)存,另外一條線程從同享內(nèi)存中讀這條消息。
但從結(jié)果來看,全部進(jìn)程就好像是1條消息被從線程A傳遞到了線程B。
這類方式之所以能實(shí)現(xiàn)消息傳遞,依托于兩點(diǎn):
所有線程都同享1片內(nèi)存,用于存儲(chǔ)同享變量;
另外,每條線程都有各自的存儲(chǔ)空間,存儲(chǔ)各自的局部變量、方法參數(shù)、異常對(duì)象。
Java采取同享內(nèi)存的方式實(shí)現(xiàn)消息傳遞,而同享內(nèi)存需要依托于同步。Java提供了synchronized、volatile關(guān)鍵字實(shí)現(xiàn)同步。另外volatile關(guān)鍵字還具有1些額外的功能。
在成員變量前加上該關(guān)鍵字便可。
public volatile boolean flag;
重排序是計(jì)算機(jī)為了提高程序履行效力而對(duì)代碼的履行順序進(jìn)行調(diào)劑。你以為代碼是1行行順序履行的,但實(shí)際并不是如此,重排序詳解請(qǐng)移步至:Java并發(fā)編程的藝術(shù)(2)——重排序
若兩行指令之間沒有依賴關(guān)系,那末計(jì)算機(jī)可以對(duì)他們的順序進(jìn)行重排序,但如果兩行之間的某個(gè)變量被volatile修飾后,重排序規(guī)則會(huì)產(chǎn)生變化。
在以下情況下,即便兩行代碼之間沒有依賴關(guān)系,也不會(huì)產(chǎn)生重排序:
volatile讀
volatile寫
“內(nèi)存可見性”指的是1條線程修改完1個(gè)同享變量后,另外一個(gè)線程若訪問這個(gè)變量將會(huì)訪問到修改后的值。即:1條線程對(duì)同享變量的修改,對(duì)其他線程立便可見。
但如果未對(duì)同享變量采取同步機(jī)制,那末同享變量的修改不會(huì)對(duì)其他線程立便可見。
通過上文可知,在Java中每條線程都有各自獨(dú)立的存儲(chǔ)空間,另外還有1個(gè)所有線程同享的內(nèi)存空間。
當(dāng)開啟線程時(shí),系統(tǒng)會(huì)將同享內(nèi)存中的所有同享變量拷貝1份到線程專屬的存儲(chǔ)空間中。接下來該線程在結(jié)束前的所有操作都是基于自己的存儲(chǔ)空間進(jìn)行的。因此,若1條線程改變了1個(gè)同享變量,僅僅改變的是這條線程專屬存儲(chǔ)空間中的變量值;此時(shí)若其他線程訪問這個(gè)變量,訪問的依然是先前從同享存儲(chǔ)空間讀出來的值。
但是我們希望1條線程將某個(gè)同享變量修改后,其他線程能立即訪問到這個(gè)最新的值,而不是失效值。
這時(shí)候就需要同步機(jī)制來解決這個(gè)問題。
要確保所有同享變量對(duì)所有線程是可見的,就需要給所有同享變量使用同步。在Java中你可以選擇將同享變量用同步代碼塊包裹或用volatile修飾同享變量。
volatile修飾了1個(gè)成員變量后,這個(gè)變量的讀寫就會(huì)比普通變量多1些步驟。
volatile變量寫
當(dāng)被volatile修飾的變量進(jìn)行寫操作時(shí),這個(gè)變量將會(huì)被直接寫入同享內(nèi)存,而非線程的專屬存儲(chǔ)空間。
volatile變量讀
當(dāng)讀取1個(gè)被volatile修飾的變量時(shí),會(huì)直接從同享內(nèi)存中讀,而非線程專屬的存儲(chǔ)空間中讀。
通過對(duì)volatile變量讀寫的限制,就可以保證線程每次讀到的都是最新的值,從而確保了該變量的內(nèi)存可見性。
進(jìn)行volatile寫操作時(shí),不但會(huì)將volatile變量寫入同享內(nèi)存,系統(tǒng)還會(huì)將當(dāng)前線程專屬空間中的所有同享變量寫入同享內(nèi)存。
進(jìn)行volatile讀操作時(shí),系統(tǒng)也會(huì)1次性將同享內(nèi)存中所有同享變量讀入線程專屬空間。
這就意味著,如果普通變量在volatile寫操作之前被修改,那末在volatile讀操作以后就可以正確讀到他們。
但是,在volatile寫操作以后被修改的普通變量 和 在volatile讀操作之前被訪問的普通變量 都不具有內(nèi)存可見性。
原子性指的是1組操作必須1起完成,中途不能被中斷。
在Java中的所有類型中,有l(wèi)ong、double類型比較特殊,他們占據(jù)8字節(jié)(64比特),其余類型都小于64比特。在32位操作系統(tǒng)中,CPU1次只能讀取/寫入32位的數(shù)據(jù),因此對(duì)64位的long、double變量的讀寫會(huì)進(jìn)行兩步。在多線程中,若1條線程只寫入了long型變量的前32位,緊接著另外一條線程讀取了這個(gè)只有“1半”的變量,從而就讀到了1個(gè)毛病的數(shù)據(jù)。
為了不這類情況,需要在用volatile修飾long、double型變量。
在內(nèi)存可見性與原子性上,volatile就相當(dāng)因而同步的setter和getter函數(shù)。但其實(shí)不具有volatile的重排序規(guī)則,同步塊只確保同步塊內(nèi)部的指令不產(chǎn)生重排序,其實(shí)不確保同步塊之外的指令的重排序。
PS1:Java中的byte居然是字節(jié),bit才是比特(位)。
PS2:char和short⑵字節(jié)、int和float⑷字節(jié)、long和double⑻字節(jié)、byte⑴字節(jié)