1.并發(fā)編程領(lǐng)域的關(guān)鍵問題1.1 線程之間的通信線程的通信是指線程之間以何種機(jī)制來交換信息。在編程中,線程之間的通信機(jī)制有兩種,共享內(nèi)存和消息傳遞。 1.2 線程間的同步同步是指程序用于控制不同線程之間操作發(fā)生相對(duì)順序的機(jī)制。 2.Java內(nèi)存模型——JMM
2.1 現(xiàn)代計(jì)算機(jī)的內(nèi)存模型物理計(jì)算機(jī)中的并發(fā)問題,物理機(jī)遇到的并發(fā)問題與虛擬機(jī)中的情況有不少相似之處,物理機(jī)對(duì)并發(fā)的處理方案對(duì)于虛擬機(jī)的實(shí)現(xiàn)也有相當(dāng)大的參考意義。 其中一個(gè)重要的復(fù)雜性來源是絕大多數(shù)的運(yùn)算任務(wù)都不可能只靠處理器“計(jì)算”就能完成,處理器至少要與內(nèi)存交互,如讀取運(yùn)算數(shù)據(jù)、存儲(chǔ)運(yùn)算結(jié)果等,這個(gè)I/O操作是很難消除的(無法僅靠寄存器來完成所有運(yùn)算任務(wù))。早期計(jì)算機(jī)中cpu和內(nèi)存的速度是差不多的,但在現(xiàn)代計(jì)算機(jī)中,cpu的指令速度遠(yuǎn)超內(nèi)存的存取速度,由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算速度有幾個(gè)數(shù)量級(jí)的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存(Cache)來作為內(nèi)存與處理器之間的緩沖:將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再從緩存同步回內(nèi)存之中,這樣處理器就無須等待緩慢的內(nèi)存讀寫了。 基于高速緩存的存儲(chǔ)交互很好地解決了處理器與內(nèi)存的速度矛盾,但是也為計(jì)算機(jī)系統(tǒng)帶來更高的復(fù)雜度,因?yàn)樗肓艘粋€(gè)新的問題:緩存一致性(Cache Coherence)。在多處理器系統(tǒng)中,每個(gè)處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存(MainMemory)。當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時(shí),將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致,舉例說明變量在多個(gè)CPU之間的共享。如果真的發(fā)生這種情況,那同步回到主內(nèi)存時(shí)以誰的緩存數(shù)據(jù)為準(zhǔn)呢?為了解決一致性的問題,需要各個(gè)處理器訪問緩存時(shí)都遵循一些協(xié)議,在讀寫時(shí)要根據(jù)協(xié)議來進(jìn)行操作,這類協(xié)議有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。
2.2 Java內(nèi)存模型(JMM)JMM定義了Java 虛擬機(jī)(JVM)在計(jì)算機(jī)內(nèi)存(RAM)中的工作方式。JVM是整個(gè)計(jì)算機(jī)虛擬模型,所以JMM是隸屬于JVM的。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。 2.2.1 JVM對(duì)Java內(nèi)存模型的實(shí)現(xiàn)
所有原始類型(boolean,byte,short,char,int,long,float,double)的局部變量都直接保存在線程棧當(dāng)中,對(duì)于它們的值各個(gè)線程之間都是獨(dú)立的。對(duì)于原始類型的局部變量,一個(gè)線程可以傳遞一個(gè)副本給另一個(gè)線程,當(dāng)它們之間是無法共享的。 2.3 Java內(nèi)存模型帶來的問題2.3.1 可見性問題CPU中運(yùn)行的線程從主存中拷貝共享對(duì)象obj到它的CPU緩存,把對(duì)象obj的count變量改為2。但這個(gè)變更對(duì)運(yùn)行在右邊CPU中的線程不可見,因?yàn)檫@個(gè)更改還沒有flush到主存中:要解決共享對(duì)象可見性這個(gè)問題,我們可以使用java volatile關(guān)鍵字或者是加鎖 2.3.2 競(jìng)爭(zhēng)現(xiàn)象線程A和線程B共享一個(gè)對(duì)象obj。假設(shè)線程A從主存讀取Obj.count變量到自己的CPU緩存,同時(shí),線程B也讀取了Obj.count變量到它的CPU緩存,并且這兩個(gè)線程都對(duì)Obj.count做了加1操作。此時(shí),Obj.count加1操作被執(zhí)行了兩次,不過都在不同的CPU緩存中。如果這兩個(gè)加1操作是串行執(zhí)行的,那么Obj.count變量便會(huì)在原始值上加2,最終主存中的Obj.count的值會(huì)是3。然而下圖中兩個(gè)加1操作是并行的,不管是線程A還是線程B先flush計(jì)算結(jié)果到主存,最終主存中的Obj.count只會(huì)增加1次變成2,盡管一共有兩次加1操作。 要解決上面的問題我們可以使用java synchronized代碼塊。 2.4 Java內(nèi)存模型中的重排序
2.4.1 重排序類型
2.4.2 重排序與依賴性
2.4.3 并發(fā)下重排序帶來的問題這里假設(shè)有兩個(gè)線程A和B,A首先執(zhí)行init ()方法,隨后B線程接著執(zhí)行use ()方法。線程B在執(zhí)行操作4時(shí),能否看到線程A在操作1對(duì)共享變量a的寫入呢?答案是:不一定能看到。 2.4.4 解決在并發(fā)下的問題1)內(nèi)存屏障——禁止重排序Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來禁止特定類型的處理器重排序,從而讓程序按我們預(yù)想的流程去執(zhí)行。 編譯器和CPU能夠重排序指令,保證最終相同的結(jié)果,嘗試優(yōu)化性能。插入一條Memory Barrier會(huì)告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序。 2)臨界區(qū)(synchronized?)臨界區(qū)內(nèi)的代碼可以重排序(但JMM不允許臨界區(qū)內(nèi)的代碼“逸出”到臨界區(qū)之外,那樣會(huì)破壞監(jiān)視器的語義)。JMM會(huì)在退出臨界區(qū)和進(jìn)入臨界區(qū)這兩個(gè)關(guān)鍵時(shí)間點(diǎn)做一些特別處理,雖然線程A在臨界區(qū)內(nèi)做了重排序,但由于監(jiān)視器互斥執(zhí)行的特性,這里的線程B根本無法“觀察”到線程A在臨界區(qū)內(nèi)的重排序。這種重排序既提高了執(zhí)行效率,又沒有改變程序的執(zhí)行結(jié)果。 2.5 Happens-Before用happens-before的概念來闡述操作之間的內(nèi)存可見性。在JMM中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見,那么這兩個(gè)操作之間必須要存在happens-before關(guān)系 。 兩個(gè)操作之間具有happens-before關(guān)系,并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行!happens-before僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對(duì)后一個(gè)操作可見,且前一個(gè)操作按順序排在第二個(gè)操作之前(the first is visible to and ordered before the second) 。 1)如果一個(gè)操作happens-before另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見,而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。(對(duì)程序員來說) 2)兩個(gè)操作之間存在happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照happens-before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序是允許的(對(duì)編譯器和處理器 來說) 在Java 規(guī)范提案中為讓大家理解內(nèi)存可見性的這個(gè)概念,提出了happens-before的概念來闡述操作之間的內(nèi)存可見性。對(duì)應(yīng)Java程序員來說,理解happens-before是理解JMM的關(guān)鍵。JMM這么做的原因是:程序員對(duì)于這兩個(gè)操作是否真的被重排序并不關(guān)心,程序員關(guān)心的是程序執(zhí)行時(shí)的語義不能被改變(即執(zhí)行結(jié)果不能被改變)。因此,happens-before關(guān)系本質(zhì)上和as-if-serial語義是一回事。as-if-serial語義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。
3.實(shí)現(xiàn)原理
3.1 volatile的內(nèi)存語義volatile變量自身具有下列特性:
volatile寫的內(nèi)存語義如下:當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。 volatile讀的內(nèi)存語義如下:當(dāng)讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量。 volatile重排序規(guī)則: volatile內(nèi)存語義的實(shí)現(xiàn)——JMM對(duì)volatile的內(nèi)存屏障插入策略: 3.1.1 volatile的實(shí)現(xiàn)原理有volatile變量修飾的共享變量進(jìn)行寫操作的時(shí)候會(huì)使用CPU提供的Lock前綴指令:
3.2 鎖的內(nèi)存語義當(dāng)線程釋放鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。。 3.2.1 synchronized的實(shí)現(xiàn)原理使用monitorenter和monitorexit指令實(shí)現(xiàn)的:
鎖的存放位置: 3.2.2 了解各種鎖鎖一共有4種狀態(tài),級(jí)別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài)。 偏向鎖:大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。無競(jìng)爭(zhēng)時(shí)不需要進(jìn)行CAS操作來加鎖和解鎖。 輕量級(jí)鎖:無競(jìng)爭(zhēng)時(shí)通過CAS操作來加鎖和解鎖。(自旋鎖——是一種鎖的機(jī)制,不是狀態(tài)) 重量級(jí)鎖:真正的加鎖操作 3.3 final的內(nèi)存語義編譯器和處理器要遵守兩個(gè)重排序規(guī)則:
final域?yàn)橐妙愋停?/p>
final語義在處理器中的實(shí)現(xiàn):
參考
作者:王偵 |
|