摘要:并發(fā)編程的挑戰(zhàn)并發(fā)編程的目的是為了讓程序運(yùn)行的更快,但是,并不是啟動(dòng)更多的線程就能讓程序最大限度的并發(fā)執(zhí)行。的實(shí)現(xiàn)原理與應(yīng)用在多線程并發(fā)編程中一直是元老級(jí)角色,很多人都會(huì)稱呼它為重量級(jí)鎖。
并發(fā)編程的挑戰(zhàn)
并發(fā)編程的目的是為了讓程序運(yùn)行的更快,但是,并不是啟動(dòng)更多的線程就能讓程序最大限度的并發(fā)執(zhí)行。如果希望通過(guò)多線程執(zhí)行任務(wù)讓程序運(yùn)行的更快,會(huì)面臨非常多的挑戰(zhàn):
(1)上下文切換
(2)死鎖
(3)資源限制(硬件和軟件)
即使是單核處理器也支持多線程執(zhí)行代碼,CPU通過(guò)給每個(gè)線程分配CPU時(shí)間片來(lái)實(shí)現(xiàn)這個(gè)機(jī)制。時(shí)間片一般只有幾十毫秒(ms)。
CPU通過(guò)時(shí)間片分配算法來(lái)循環(huán)執(zhí)行任務(wù),當(dāng)前任務(wù)執(zhí)行一個(gè)時(shí)間片后會(huì)切換到下一個(gè)任務(wù)。但是,在切換前會(huì)保存上一個(gè)任務(wù)的狀態(tài),以便下次切換回這個(gè)任務(wù)時(shí),可以再加載這個(gè)任務(wù)的狀態(tài)。所以任務(wù)從保存到再加載的過(guò)程就是一次上下文切換。上下文切換會(huì)影響多線程執(zhí)行的速度。
使用Lmbench3可以測(cè)量上下文切換的時(shí)長(zhǎng)。
使用vmstat可以測(cè)量上下文切換的次數(shù)。
vmstat 1 :測(cè)試一秒鐘上下文切換的次數(shù)。
CS(Context Switch)表示上下文切換的次數(shù)。
如何減少上下文切換?
(1)無(wú)鎖并發(fā)編程(將數(shù)據(jù)的ID按照Hash算法取模分段,不同線程處理不同段的數(shù)據(jù))
(2)CAS算法(Java的Atomic包使用CAS算法來(lái)更新數(shù)據(jù),而不需要枷鎖)
(3)使用最少線程(避免創(chuàng)建不需要的線程,比如任務(wù)很少,卻創(chuàng)建了很多線程,導(dǎo)致大量線程處于等待狀態(tài))
(4)協(xié)程(在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度,并在單線程里維護(hù)多個(gè)任務(wù)間的切換)
實(shí)戰(zhàn):減少上下文切換?
通過(guò)減少線上大量WAITING的線程,來(lái)減少上下文切換次數(shù)。
第一步:用jstack命令dump線程信息,看看pid為3117的進(jìn)程里的線程都在做什么。
/java/bin/jstack 31177 > /home/dump17
第二步:統(tǒng)計(jì)所有線程分別處于什么狀態(tài),發(fā)現(xiàn)300多個(gè)線程處于WAITING狀態(tài)。
grep java.lang.Thread.State dump17 | awk "{print $2$3$4$5}" | sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobjectmonitor)
3 TIMED_WAITING(parking)
第三步:打開dump文件查看處于WAITING(onobjectmonitor)的線程在做什么。發(fā)現(xiàn)這些線程基本全是JBOSS的工作線程,說(shuō)明JBOSS線程池里線程接收的任務(wù)太少,大量線程都閑著。
第四步:減少JBOSS的工作線程數(shù),找到JBOSS線程池配置信息,將maxThreads降到100。
第五步: 重啟,發(fā)現(xiàn)WAITING減少了175個(gè)。
一旦出現(xiàn)死鎖,業(yè)務(wù)是可感知的,因?yàn)椴荒芾^續(xù)提供服務(wù)了,那么只能通過(guò)dump線程查看到底哪個(gè)線程出現(xiàn)了問(wèn)題。
避免死鎖的幾個(gè)常見(jiàn)方法:
(1)避免一個(gè)線程同時(shí)獲取多個(gè)鎖
(2)避免一個(gè)線程在鎖內(nèi)同時(shí)占用多個(gè)資源,盡量保證每個(gè)鎖只占用一個(gè)資源。
(3)嘗試使用定時(shí)鎖,使用lock.tryLock(timeout)來(lái)替代使用內(nèi)部鎖機(jī)制。
(3)對(duì)于數(shù)據(jù)庫(kù)鎖,加鎖和解鎖必須在一個(gè)數(shù)據(jù)庫(kù)連接里,否則會(huì)出現(xiàn)解鎖失敗的情況。
資源限制是指在進(jìn)行并發(fā)編程時(shí),程序的執(zhí)行速度受限于計(jì)算機(jī)硬件資源或軟件資源。
硬件資源限制:帶寬,硬盤讀寫速度,CPU處理速度。
軟件資源限制:數(shù)據(jù)庫(kù)的連接數(shù)和socket連接數(shù)。
對(duì)于java開發(fā)工程師而言,強(qiáng)烈建議多使用JDK并發(fā)包提供的并發(fā)容器和工具類來(lái)解決并發(fā)問(wèn)題,以為這些類都已經(jīng)通過(guò)了充分的測(cè)試和優(yōu)化,均可解決本章提到的幾個(gè)挑戰(zhàn)。
Java中所使用的并發(fā)機(jī)制依賴于JVM的實(shí)現(xiàn)和CPU的指令。
Java代碼——>Java字節(jié)碼——>JVM——>匯編指令——>CPU上執(zhí)行。
Volatile的應(yīng)用
可見(jiàn)性:當(dāng)一個(gè)線程修改一個(gè)共享變量時(shí),另外一個(gè)線程能讀到這個(gè)修改的值。
在多線程并發(fā)編程中synchronized和volatile都扮演著重要的角色,volatile是輕量級(jí)的synchronized,它在多處理器開發(fā)中保證了共享變量的"可見(jiàn)性"。
volatile比synchronized的使用和執(zhí)行成本更低,以為它不會(huì)引起線程上下文的切換和調(diào)度。
volatile定義:
Java語(yǔ)言規(guī)范第3版中對(duì)volatile的定義如下:Java編程語(yǔ)言允許線程訪問(wèn)共享變量,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過(guò)排它鎖多帶帶獲得這個(gè)變量。Java語(yǔ)言提供了volatile,在某些情況下比鎖更加方便。如果一個(gè)字段被聲明稱volatile,Java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的。
術(shù)語(yǔ) | 描述 |
---|---|
內(nèi)存屏障 | 是一組處理器指令,用于實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制 |
原子操作 | 不可中斷的一個(gè)或一些列操作 |
緩存行填充 | 當(dāng)處理器識(shí)別到從內(nèi)存中讀取的操作數(shù)是可緩存的,處理器讀取整個(gè)高速緩存行到適當(dāng)?shù)木彺妫↙1,L2,L3的或所有) |
緩存命中 | 如果進(jìn)行高速緩存行填充操作的內(nèi)存位置仍然是下次處理器訪問(wèn)的地址時(shí),處理器從緩存中讀取操作數(shù),而不是從內(nèi)存讀取 |
寫命中 | 當(dāng)處理器將操作數(shù)寫回到一個(gè)內(nèi)存緩存的區(qū)域時(shí),它首先會(huì)檢查這個(gè)緩存的內(nèi)存地址是否在緩存行中,如果存在一個(gè)有效的緩存行,則處理器將這個(gè)操作數(shù)寫回到緩存,而不是寫回到內(nèi)存,這個(gè)操作被稱為寫命中。 |
寫缺失 | 一個(gè)有效的緩存行被寫入到不存在的內(nèi)存區(qū)域。 |
volatile是如何來(lái)保證可見(jiàn)性的呢?讓我們?cè)赬86處理器下通過(guò)工具獲取JIT編譯器生成的
匯編指令來(lái)查看對(duì)volatile進(jìn)行寫操作時(shí),CPU會(huì)做什么事情。
Java代碼如下
instance = new Singleton(); // instance是volatile變量
轉(zhuǎn)變成匯編代碼,如下
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
有volatile變量修飾的共享變量進(jìn)行寫操作的時(shí)候會(huì)多出第二行匯編代碼,通過(guò)查IA-32架
構(gòu)軟件開發(fā)者手冊(cè)可知,Lock前綴的指令在多核處理器下會(huì)引發(fā)了兩件事情。
1)將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。
2)這個(gè)寫回內(nèi)存的操作會(huì)使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效。
為了提高處理速度,處理器不直接和內(nèi)存進(jìn)行通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部
緩存(L1,L2或其他)后再進(jìn)行操作,但操作完不知道何時(shí)會(huì)寫到內(nèi)存。如果對(duì)聲明了volatile的變量進(jìn)行寫操作,JVM就會(huì)向處理器發(fā)送一條Lock前綴的指令,將這個(gè)變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。但是,就算寫回到內(nèi)存,如果其他處理器緩存的值還是舊的,再執(zhí)行計(jì)算操作就會(huì)有問(wèn)題。所以,在多處理器下,為了保證各個(gè)處理器的緩存是一致的,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議,每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了,當(dāng)
處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀
態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存
里。
下面來(lái)具體講解volatile的兩條實(shí)現(xiàn)原則
1)Lock前綴指令會(huì)引起處理器緩存回寫到內(nèi)存。Lock前綴指令導(dǎo)致在執(zhí)行指令期間,聲
言處理器的LOCK#信號(hào)。在多處理器環(huán)境中,LOCK#信號(hào)確保在聲言該信號(hào)期間,處理器可以
獨(dú)占任何共享內(nèi)存。但是,在最近的處理器里,LOCK#信號(hào)一般不鎖總線,而是鎖緩存,畢
竟鎖總線開銷的比較大。在8.1.4節(jié)有詳細(xì)說(shuō)明鎖定操作對(duì)處理器緩存的影響,對(duì)于Intel486和
Pentium處理器,在鎖操作時(shí),總是在總線上聲言LOCK#信號(hào)。但在P6和目前的處理器中,如果訪問(wèn)的內(nèi)存區(qū)域已經(jīng)緩存在處理器內(nèi)部,則不會(huì)聲言LOCK#信號(hào)。相反,它會(huì)鎖定這塊內(nèi)存區(qū)域的緩存并回寫到內(nèi)存,并使用緩存一致性機(jī)制來(lái)確保修改的原子性,此操作被稱為“緩存鎖
定”,緩存一致性機(jī)制會(huì)阻止同時(shí)修改由兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù)。
2)一個(gè)處理器的緩存回寫到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存無(wú)效。IA-32處理器和Intel 64處
理器使用MESI(修改、獨(dú)占、共享、無(wú)效)控制協(xié)議去維護(hù)內(nèi)部緩存和其他處理器緩存的一致
性。在多核處理器系統(tǒng)中進(jìn)行操作的時(shí)候,IA-32和Intel 64處理器能嗅探其他處理器訪問(wèn)系統(tǒng)內(nèi)存和它們的內(nèi)部緩存。處理器使用嗅探技術(shù)保證它的內(nèi)部緩存、系統(tǒng)內(nèi)存和其他處理器的緩存的數(shù)據(jù)在總線上保持一致。例如,在Pentium和P6 family處理器中,如果通過(guò)嗅探一個(gè)處理器來(lái)檢測(cè)其他處理器打算寫內(nèi)存地址,而這個(gè)地址當(dāng)前處于共享狀態(tài),那么正在嗅探的處理器將使它的緩存行無(wú)效,在下次訪問(wèn)相同內(nèi)存地址時(shí),強(qiáng)制執(zhí)行緩存行填充。
在多線程并發(fā)編程中synchronized一直是元老級(jí)角色,很多人都會(huì)稱呼它為重量級(jí)鎖。但是,隨著Java SE 1.6對(duì)synchronized進(jìn)行了各種優(yōu)化之后,有些情況下它就并不那么重了。本文詳細(xì)介紹Java SE 1.6中為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗而引入的偏向鎖和輕量級(jí)鎖,以及鎖的存儲(chǔ)結(jié)構(gòu)和升級(jí)過(guò)程。
先來(lái)看下利用synchronized實(shí)現(xiàn)同步的基礎(chǔ):Java中的每一個(gè)對(duì)象都可以作為鎖。具體表現(xiàn)
為以下3種形式。
對(duì)于普通同步方法,鎖是當(dāng)前實(shí)例對(duì)象。
對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的Class對(duì)象。
對(duì)于同步方法塊,鎖是Synchonized括號(hào)里配置的對(duì)象。
當(dāng)一個(gè)線程試圖訪問(wèn)同步代碼塊時(shí),它首先必須得到鎖,退出或拋出異常時(shí)必須釋放鎖。
那么鎖到底存在哪里呢?鎖里面會(huì)存儲(chǔ)什么信息呢?
從JVM規(guī)范中可以看到Synchonized在JVM里的實(shí)現(xiàn)原理,JVM基于進(jìn)入和退出Monitor對(duì)象來(lái)實(shí)現(xiàn)方法同步和代碼塊同步,但兩者的實(shí)現(xiàn)細(xì)節(jié)不一樣。代碼塊同步是使用monitorenter和monitorexit指令實(shí)現(xiàn)的,而方法同步是使用另外一種方式實(shí)現(xiàn)的,細(xì)節(jié)在JVM規(guī)范里并沒(méi)有詳細(xì)說(shuō)明。但是,方法的同步同樣可以使用這兩個(gè)指令來(lái)實(shí)現(xiàn)。
monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結(jié)束處和異常處,JVM要保證每個(gè)monitorenter必須有對(duì)應(yīng)的monitorexit與之配對(duì)。任何對(duì)象都有一個(gè)monitor與之關(guān)聯(lián),當(dāng)且一個(gè)monitor被持有后,它將處于鎖定狀態(tài)。線程執(zhí)行monitorenter指令時(shí),將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的monitor的所有權(quán),即嘗試獲得對(duì)象的鎖。
Java對(duì)象頭
synchronized用的鎖是存在Java對(duì)象頭里的。
鎖的升級(jí)與對(duì)比
Java SE 1.6為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗,引入了“偏向鎖”和“輕量級(jí)鎖”,在Java SE 1.6中,鎖一共有4種狀態(tài),級(jí)別從低到高依次是:無(wú)鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài),這幾個(gè)狀態(tài)會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)。鎖可以升級(jí)但不能降級(jí),意味著偏向鎖升級(jí)成輕量級(jí)鎖后不能降級(jí)成偏向鎖。這種鎖升級(jí)卻不能降級(jí)的策略,目的是為了提高獲得鎖和釋放鎖的效率,下文會(huì)詳細(xì)分析。
1.偏向鎖
HotSpot 的作者經(jīng)過(guò)研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同
一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。當(dāng)一個(gè)線程訪問(wèn)同步塊并
獲取鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程ID,以后該線程在進(jìn)入和退出
同步塊時(shí)不需要進(jìn)行CAS操作來(lái)加鎖和解鎖,只需簡(jiǎn)單地測(cè)試一下對(duì)象頭的Mark Word里是否
存儲(chǔ)著指向當(dāng)前線程的偏向鎖。如果測(cè)試成功,表示線程已經(jīng)獲得了鎖。如果測(cè)試失敗,則需
要再測(cè)試一下Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒(méi)有設(shè)置,則使用CAS競(jìng)爭(zhēng)鎖;如果設(shè)置了,則嘗試使用CAS將對(duì)象頭的偏向鎖指向當(dāng)前線程。
(1)偏向鎖的撤銷
偏向鎖使用了一種等到競(jìng)爭(zhēng)出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖。偏向鎖的撤銷,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒(méi)有正在執(zhí)行的字節(jié)碼)。它會(huì)首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動(dòng)狀態(tài),則將對(duì)象頭設(shè)置成無(wú)鎖狀態(tài);如果線程仍然活著,擁有偏向鎖的棧會(huì)被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對(duì)象頭的Mark Word要么重新偏向于其他線程,要么恢復(fù)到無(wú)鎖或者標(biāo)記對(duì)象不適合作為偏向鎖,最后喚醒暫停的線程。
(2)偏向鎖的撤銷
偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來(lái)關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),可以通過(guò)JVM參數(shù)關(guān)閉偏向鎖:XX:UseBiasedLocking=false,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。
2.輕量級(jí)鎖
(1)輕量級(jí)鎖加鎖
線程在執(zhí)行同步塊之前,JVM會(huì)先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對(duì)象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對(duì)象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競(jìng)爭(zhēng)鎖,當(dāng)前線程便嘗試使用自旋來(lái)獲取鎖。
(2)輕量級(jí)鎖解鎖
輕量級(jí)解鎖時(shí),會(huì)使用原子的CAS操作將Displaced Mark Word替換回到對(duì)象頭,如果成功,則表示沒(méi)有競(jìng)爭(zhēng)發(fā)生。如果失敗,表示當(dāng)前鎖存在競(jìng)爭(zhēng),鎖就會(huì)膨脹成重量級(jí)鎖。
因?yàn)樽孕龝?huì)消耗CPU,為了避免無(wú)用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級(jí)
成重量級(jí)鎖,就不會(huì)再恢復(fù)到輕量級(jí)鎖狀態(tài)。當(dāng)鎖處于這個(gè)狀態(tài)下,其他線程試圖獲取鎖時(shí),
都會(huì)被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會(huì)喚醒這些線程,被喚醒的線程就會(huì)進(jìn)行新一輪
的奪鎖之爭(zhēng)。
3.鎖的優(yōu)缺點(diǎn)對(duì)比
鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 使用場(chǎng)景 | |
---|---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距 | 如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來(lái)額外的鎖撤銷的消耗 | 適用于只有一個(gè)線程訪問(wèn)同步塊場(chǎng)景 | |
輕量級(jí)鎖 | 競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了程序的響應(yīng)速度 | 如果始終得不到鎖競(jìng)爭(zhēng)的線程,使用自旋會(huì)消耗CPU | 追求響應(yīng)時(shí)間,同步塊執(zhí)行速度非常快 | |
重量級(jí)鎖 | 線程競(jìng)爭(zhēng)不適用自旋,不會(huì)消耗CPU | 線程阻塞,響應(yīng)時(shí)間緩慢 | 最求吞吐量,同步塊執(zhí)行速度較慢 |
原子操作的實(shí)現(xiàn)原理
原子(atomic)本意是“不能被進(jìn)一步分割的最小粒子”,而原子操作(atomic operation)意
為“不可被中斷的一個(gè)或一系列操作”。在多處理器上實(shí)現(xiàn)原子操作就變得有點(diǎn)復(fù)雜。讓我們
一起來(lái)聊一聊在Intel處理器和Java里是如何實(shí)現(xiàn)原子操作的。
1.術(shù)語(yǔ)定義
術(shù)語(yǔ)名稱 | 英文 | 解釋 | |
---|---|---|---|
緩存行 | Cache line | 緩存的最小操作單位 | |
比較并交換 | Compare And Swap | CAS操作需要輸入兩個(gè)數(shù)值,一個(gè)舊值(期望操作前的值)和一個(gè)新值,在操作期間先比較舊值有沒(méi)有發(fā)生變化,如果沒(méi)有發(fā)生變化,才交換成新值,發(fā)生了變化則不交換 | |
CPU流水線 | CPU pipeline | CPU流水線的工作方式就像工業(yè)生產(chǎn)上的裝配流水線,在CPU中由5~6個(gè)不同功能的電路單元組成一條指令處理流水線,然后將一個(gè)X86指令分成5~6步后再由這些電路單元分別執(zhí)行,這樣就能實(shí)現(xiàn)在一個(gè)CPU時(shí)鐘周期完成一條指令,因此提高CPU的運(yùn)算速度 | |
內(nèi)存順序沖突 | Memory order violation | 內(nèi)存順序沖突一般是由假共享引起的,假共享是指多個(gè)CPU同時(shí)修改同一個(gè)緩存行的不同部分而引起其中一個(gè)CPU的操作無(wú)效,當(dāng)出現(xiàn)這個(gè)內(nèi)存順序沖突時(shí),CPU必須清空流水線 |
2.處理器如何實(shí)現(xiàn)原子操作
32位IA-32處理器使用基于對(duì)緩存加鎖或總線加鎖的方式來(lái)實(shí)現(xiàn)多處理器之間的原子操作。首先處理器會(huì)自動(dòng)保證基本的內(nèi)存操作的原子性。處理器保證從系統(tǒng)內(nèi)存中讀取或者寫入一個(gè)字節(jié)是原子的,意思是當(dāng)一個(gè)處理器讀取一個(gè)字節(jié)時(shí),其他處理器不能訪問(wèn)這個(gè)字節(jié)的內(nèi)存地址。Pentium 6和最新的處理器能自動(dòng)保證單處理器對(duì)同一個(gè)緩存行里進(jìn)行16/32/64位的操作是原子的,但是復(fù)雜的內(nèi)存操作處理器是不能自動(dòng)保證其原子性的,比如跨總線寬度、跨多個(gè)緩存行和跨頁(yè)表的訪問(wèn)。但是,處理器提供總線鎖定和緩存鎖定兩個(gè)機(jī)制來(lái)保證復(fù)雜內(nèi)存操作的原子性。
(1)使用總線鎖保證原子性
第一個(gè)機(jī)制是通過(guò)總線鎖保證原子性。如果多個(gè)處理器同時(shí)對(duì)共享變量進(jìn)行讀改寫操作(i++就是經(jīng)典的讀改寫操作),那么共享變量就會(huì)被多個(gè)處理器同時(shí)進(jìn)行操作,這樣讀改寫操作就不是原子的,操作完之后共享變量的值會(huì)和期望的不一致。舉個(gè)例子,如果i=1,我們進(jìn)行兩次i++操作,我們期望的結(jié)果是3,但是有可能結(jié)果是2。
原因可能是多個(gè)處理器同時(shí)從各自的緩存中讀取變量i,分別進(jìn)行加1操作,然后分別寫入系統(tǒng)內(nèi)存中。那么,想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時(shí)候,CPU2不能操作緩存了該共享變量?jī)?nèi)存地址的緩存。
處理器使用總線鎖就是來(lái)解決這個(gè)問(wèn)題的。所謂總線鎖就是使用處理器提供的一個(gè)LOCK#信號(hào),當(dāng)一個(gè)處理器在總線上輸出此信號(hào)時(shí),其他處理器的請(qǐng)求將被阻塞住,那么該處理器可以獨(dú)占共享內(nèi)存。
(2)使用緩存鎖保證原子性
第二個(gè)機(jī)制是通過(guò)緩存鎖定來(lái)保證原子性。在同一時(shí)刻,我們只需保證對(duì)某個(gè)內(nèi)存地址的操作是原子性即可,但總線鎖定把CPU和內(nèi)存之間的通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內(nèi)存地址的數(shù)據(jù),所以總線鎖定的開銷比較大,目前處理器在某些場(chǎng)合下使用緩存鎖定代替總線鎖定來(lái)進(jìn)行優(yōu)化。
頻繁使用的內(nèi)存會(huì)緩存在處理器的L1、L2和L3高速緩存里,那么原子操作就可以直接在處理器內(nèi)部緩存中進(jìn)行,并不需要聲明總線鎖,在Pentium 6和目前的處理器中可以使用“緩存鎖定”的方式來(lái)實(shí)現(xiàn)復(fù)雜的原子性。所謂“緩存鎖定”是指內(nèi)存區(qū)域如果被緩存在處理器的緩存行中,并且在Lock操作期間被鎖定,那么當(dāng)它執(zhí)行鎖操作回寫到內(nèi)存時(shí),處理器不在總線上聲言LOCK#信號(hào),而是修改內(nèi)部的內(nèi)存地址,并允許它的緩存一致性機(jī)制來(lái)保證操作的原子性,因?yàn)榫彺嬉恢滦詸C(jī)制會(huì)阻止同時(shí)修改由兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù),當(dāng)其他處理器回寫已被鎖定的緩存行的數(shù)據(jù)時(shí),會(huì)使緩存行無(wú)效,在如圖2-3所示的例子中,當(dāng)CPU1修改緩存行中的i時(shí)使用了緩存鎖定,那么CPU2就不能同時(shí)緩存i的緩存行。
但是有兩種情況下處理器不會(huì)使用緩存鎖定。
第一種情況是:當(dāng)操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部,或操作的數(shù)據(jù)跨多個(gè)緩存行(cache line)時(shí),則處理器會(huì)調(diào)用總線鎖定。
第二種情況是:有些處理器不支持緩存鎖定。對(duì)于Intel 486和Pentium處理器,就算鎖定的內(nèi)存區(qū)域在處理器的緩存行中也會(huì)調(diào)用總線鎖定。
針對(duì)以上兩個(gè)機(jī)制,我們通過(guò)Intel處理器提供了很多Lock前綴的指令來(lái)實(shí)現(xiàn)。例如,位測(cè)試和修改指令:BTS、BTR、BTC;交換指令XADD、CMPXCHG,以及其他一些操作數(shù)和邏輯指令(如ADD、OR)等,被這些指令操作的內(nèi)存區(qū)域就會(huì)加鎖,導(dǎo)致其他處理器不能同時(shí)訪問(wèn)它。
3.Java如何實(shí)現(xiàn)原子操作
在Java中可以通過(guò)鎖和循環(huán)CAS的方式來(lái)實(shí)現(xiàn)原子操作。
(1)使用循環(huán)CAS實(shí)現(xiàn)原子操作
JVM中的CAS操作正是利用了處理器提供的CMPXCHG指令實(shí)現(xiàn)的。自旋CAS實(shí)現(xiàn)的基本思路就是循環(huán)進(jìn)行CAS操作直到成功為止。
(2)CAS實(shí)現(xiàn)原子操作的三大問(wèn)題
在Java并發(fā)包中有一些并發(fā)框架也使用了自旋CAS的方式來(lái)實(shí)現(xiàn)原子操作比如LinkedTransferQueue類的Xfer方法。CAS雖然很高效地解決了原子操作,但是CAS仍然存在三
大問(wèn)題。ABA問(wèn)題,循環(huán)時(shí)間長(zhǎng)開銷大,以及只能保證一個(gè)共享變量的原子操作。
1)ABA問(wèn)題。因?yàn)镃AS需要在操作值的時(shí)候,檢查值有沒(méi)有發(fā)生變化,如果沒(méi)有發(fā)生變化
則更新,但是如果一個(gè)值原來(lái)是A,變成了B,又變成了A,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它
的值沒(méi)有發(fā)生變化,但是實(shí)際上卻變化了。ABA問(wèn)題的解決思路就是使用版本號(hào)。在變量前面
追加上版本號(hào),每次變量更新的時(shí)候把版本號(hào)加1,那么A→B→A就會(huì)變成1A→2B→3A。從
Java 1.5開始,JDK的Atomic包里提供了一個(gè)類AtomicStampedReference來(lái)解決ABA問(wèn)題。這個(gè)類的compareAndSet方法的作用是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且檢查當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值。
2)循環(huán)時(shí)間長(zhǎng)開銷大。自旋CAS如果長(zhǎng)時(shí)間不成功,會(huì)給CPU帶來(lái)非常大的執(zhí)行開銷。如果JVM能支持處理器提供的pause指令,那么效率會(huì)有一定的提升。pause指令有兩個(gè)作用:第一,它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會(huì)消耗過(guò)多的執(zhí)行資源,延遲的時(shí)間取決于具體實(shí)現(xiàn)的版本,在一些處理器上延遲時(shí)間是零;第二,它可以避免在退出循環(huán)的時(shí)候因內(nèi)存順序沖突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush)從而提高CPU的執(zhí)行效率。
3)只能保證一個(gè)共享變量的原子操作。當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來(lái)保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無(wú)法保證操作的原子性,這個(gè)時(shí)候就可以用鎖。還有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來(lái)操作。比如,有兩個(gè)共享變量i=2,j=a,合并一下ij=2a,然后用CAS來(lái)操作ij。從Java 1.5開始,JDK提供了AtomicReference類來(lái)保證引用對(duì)象之間的原子性,就可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行CAS操作。
(3)使用鎖機(jī)制實(shí)現(xiàn)原子操作
鎖機(jī)制保證了只有獲得鎖的線程才能夠操作鎖定的內(nèi)存區(qū)域。JVM內(nèi)部實(shí)現(xiàn)了很多種鎖機(jī)制,有偏向鎖、輕量級(jí)鎖和互斥鎖。有意思的是除了偏向鎖,JVM實(shí)現(xiàn)鎖的方式都用了循環(huán)CAS,即當(dāng)一個(gè)線程想進(jìn)入同步塊的時(shí)候使用循環(huán)CAS的方式來(lái)獲取鎖,當(dāng)它退出同步塊的時(shí)候使用循環(huán)CAS釋放鎖。
Java線程之間的通信對(duì)程序員完全透明,內(nèi)存可見(jiàn)性問(wèn)題很容易困擾Java程序員。
Java內(nèi)存模型的基礎(chǔ)
在并發(fā)編程中,需要處理兩個(gè)關(guān)鍵問(wèn)題:線程之間如何通信及線程之間如何同步(這里的線程是指并發(fā)執(zhí)行的活動(dòng)實(shí)體)。通信是指線程之間以何種機(jī)制來(lái)交換信息。在命令式編程中,線程之間的通信機(jī)制有兩種:共享內(nèi)存和消息傳遞。
在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),通過(guò)寫-讀內(nèi)存中的公共狀態(tài)進(jìn)行隱式通信。在消息傳遞的并發(fā)模型里,線程之間沒(méi)有公共狀態(tài),線程之間必須通過(guò)發(fā)送消息來(lái)顯式進(jìn)行通信。
同步是指程序中用于控制不同線程間操作發(fā)生相對(duì)順序的機(jī)制。在共享內(nèi)存并發(fā)模型里,同步是顯式進(jìn)行的。程序員必須顯式指定某個(gè)方法或某段代碼需要在線程之間互斥執(zhí)行。在消息傳遞的并發(fā)模型里,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式進(jìn)行的。
Java的并發(fā)采用的是共享內(nèi)存模型,Java線程之間的通信總是隱式進(jìn)行,整個(gè)通信過(guò)程對(duì)程序員完全透明。如果編寫多線程程序的Java程序員不理解隱式進(jìn)行的線程之間通信的工作機(jī)制,很可能會(huì)遇到各種奇怪的內(nèi)存可見(jiàn)性問(wèn)題。
Java內(nèi)存模型的抽象結(jié)構(gòu)
在Java中,所有實(shí)例域、靜態(tài)域和數(shù)組元素都存儲(chǔ)在堆內(nèi)存中,堆內(nèi)存在線程之間共享(本章用“共享變量”這個(gè)術(shù)語(yǔ)代指實(shí)例域,靜態(tài)域和數(shù)組元素)。局部變量(Local Variables),方法定義參數(shù)(Java語(yǔ)言規(guī)范稱之為Formal Method Parameters)和異常處理器參數(shù)(Exception Handler Parameters)不會(huì)在線程之間共享,它們不會(huì)有內(nèi)存可見(jiàn)性問(wèn)題,也不受內(nèi)存模型的影響。
Java線程之間的通信由Java內(nèi)存模型(本文簡(jiǎn)稱為JMM)控制,JMM決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見(jiàn)。從抽象的角度來(lái)看,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)化。Java內(nèi)存模型的抽象示意如圖3-1所示。
從圖3-1來(lái)看,如果線程A與線程B之間要通信的話,必須要經(jīng)歷下面2個(gè)步驟。
1)線程A把本地內(nèi)存A中更新過(guò)的共享變量刷新到主內(nèi)存中去。
2)線程B到主內(nèi)存中去讀取線程A之前已更新過(guò)的共享變量。
下面通過(guò)示意圖(見(jiàn)圖3-2)來(lái)說(shuō)明這兩個(gè)步驟。
如圖3-2所示,本地內(nèi)存A和本地內(nèi)存B由主內(nèi)存中共享變量x的副本。假設(shè)初始時(shí),這3個(gè)內(nèi)存中的x值都為0。線程A在執(zhí)行時(shí),把更新后的x值(假設(shè)值為1)臨時(shí)存放在自己的本地內(nèi)存A中。當(dāng)線程A和線程B需要通信時(shí),線程A首先會(huì)把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中,此時(shí)主內(nèi)存中的x值變?yōu)榱?。隨后,線程B到主內(nèi)存中去讀取線程A更新后的x值,此時(shí)線程B的本地內(nèi)存的x值也變?yōu)榱?。
從整體來(lái)看,這兩個(gè)步驟實(shí)質(zhì)上是線程A在向線程B發(fā)送消息,而且這個(gè)通信過(guò)程必須要經(jīng)過(guò)主內(nèi)存。JMM通過(guò)控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來(lái)為Java程序員提供內(nèi)存可見(jiàn)性保證。
從源代碼到指令序列的重排序
在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序。重排序分3種類型。
1)編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。
2)指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level
Parallelism,ILP)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)
機(jī)器指令的執(zhí)行順序。
3)內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
從Java源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)分別經(jīng)歷下面3種重排序,如圖3-3所示。
上述的1屬于編譯器重排序,2和3屬于處理器重排序。這些重排序可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題。對(duì)于編譯器,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對(duì)于處理器重排序,JMM的處理器重排序規(guī)則會(huì)要求Java編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障(Memory Barriers,Intel稱之為Memory Fence)指令,通過(guò)內(nèi)存屏障指令來(lái)禁止特定類型的處理器重排序。
JMM屬于語(yǔ)言級(jí)的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺(tái)之上,通過(guò)禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見(jiàn)性保證。
并發(fā)編程模型的分類
為了保證內(nèi)存可見(jiàn)性,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來(lái)禁止特定類型的處理器重排序。JMM把內(nèi)存屏障指令分為4類。
StoreLoad Barriers是一個(gè)“全能型”的屏障,它同時(shí)具有其他3個(gè)屏障的效果?,F(xiàn)代的多處
理器大多支持該屏障(其他類型的屏障不一定被所有處理器支持)。執(zhí)行該屏障開銷會(huì)很昂
貴,因?yàn)楫?dāng)前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中(Buffer Fully Flush)。
happens-before簡(jiǎn)介
JSR-133使用happens-before的概念來(lái)闡述操作之間的內(nèi)存可見(jiàn)性。在JMM中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見(jiàn),那么這兩個(gè)操作之間必須要存在happens-before關(guān)系。這里提到的兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間。
與程序員密切相關(guān)的happens-before規(guī)則如下。
程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作。
監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖。
volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的
讀。
傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
注意兩個(gè)操作之間具有happens-before關(guān)系,并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行!happens-before僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對(duì)后一個(gè)操作可見(jiàn),且前一個(gè)操作按順序排在第二個(gè)操作之前(the first is visible to and ordered before the second)。happens-before的定義很微妙,后文會(huì)具體說(shuō)明happens-before為什么要這么定義。
happens-before與JMM的關(guān)系如圖3-5所示。
如圖3-5所示,一個(gè)happens-before規(guī)則對(duì)應(yīng)于一個(gè)或多個(gè)編譯器和處理器重排序規(guī)則。對(duì)于Java程序員來(lái)說(shuō),happens-before規(guī)則簡(jiǎn)單易懂,它避免Java程序員為了理解JMM提供的內(nèi)存
可見(jiàn)性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實(shí)現(xiàn)方法。
重排序
重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重新排序的一種手段。
數(shù)據(jù)依賴性
如果兩個(gè)操作訪問(wèn)同一個(gè)變量,且這兩個(gè)操作中有一個(gè)為寫操作,此時(shí)這兩個(gè)操作之間就存在數(shù)據(jù)依賴性。數(shù)據(jù)依賴分為下列3種類型,如表3-4所示。
名稱 | 示例代碼 | 說(shuō)明 |
---|---|---|
寫后讀 | a=1;b=a; | 寫一個(gè)變量之后,再讀這個(gè) 位置 |
寫后寫 | a=1;a=2; | 寫一個(gè)變量之后,再寫這個(gè)變量 |
讀后寫 | a=b;b=1; | 讀一個(gè)變量之后,再寫這個(gè)變量 |
上面3種情況,只要重排序兩個(gè)操作的執(zhí)行順序,程序的執(zhí)行結(jié)果就會(huì)被改變。
前面提到過(guò),編譯器和處理器可能會(huì)對(duì)操作做重排序。編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。
這里所說(shuō)的數(shù)據(jù)依賴性僅針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。
as-if-serial語(yǔ)義
as-if-serial語(yǔ)義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)
程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語(yǔ)義。
為了遵守as-if-serial語(yǔ)義,編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果。但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。為了具體說(shuō)明,請(qǐng)看下面計(jì)算圓面積的代碼示例。
double pi = 3.14; // A
double r = 1.0; // B
double area = pi r r; // C
A和C之間存在數(shù)據(jù)依賴關(guān)系,同時(shí)B和C之間也存在數(shù)據(jù)依賴關(guān)系。因此在最終執(zhí)行的指令序列中,C不能被重排序到A和B的前面。但A和B之間沒(méi)有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以重排序A和B之間的執(zhí)行順序。
程序順序規(guī)則
根據(jù)happens-before的程序順序規(guī)則,上面計(jì)算圓的面積的示例代碼存在3個(gè)happens-before關(guān)系。
1)A happens-before B。
2)B happens-before C。
3)A happens-before C。
這里A happens-before B,但實(shí)際執(zhí)行時(shí)B卻可以排在A之前執(zhí)行(看上面的重排序后的執(zhí)
行順序)。如果A happens-before B,JMM并不要求A一定要在B之前執(zhí)行。JMM僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對(duì)后一個(gè)操作可見(jiàn),且前一個(gè)操作按順序排在第二個(gè)操作之前。這里操作A的執(zhí)行結(jié)果不需要對(duì)操作B可見(jiàn);而且重排序操作A和操作B后的執(zhí)行結(jié)果,與操作A和操作B按happens-before順序執(zhí)行的結(jié)果一致。在這種情況下,JMM會(huì)認(rèn)為這種重排序并不非法(not illegal),JMM允許這種重排序。
在計(jì)算機(jī)中,軟件技術(shù)和硬件技術(shù)有一個(gè)共同的目標(biāo):在不改變程序執(zhí)行結(jié)果的前提下,盡可能提高并行度。編譯器和處理器遵從這一目標(biāo),從happens-before的定義我們可以看出,JMM同樣遵從這一目標(biāo)。
重排序?qū)Χ嗑€程的影響
class ReorderExample { int a = 0; boolean flag = false; public void writer() { a = 1; // 1 flag = true; // 2 } Public void reader() { if (flag) { // 3 int i = a * a; // 4 …… } } }
flag變量是個(gè)標(biāo)記,用來(lái)標(biāo)識(shí)變量a是否已被寫入。這里假設(shè)有兩個(gè)線程A和B,A首先執(zhí)行
writer()方法,隨后B線程接著執(zhí)行reader()方法。線程B在執(zhí)行操作4時(shí),能否看到線程A在操作
1對(duì)共享變量a的寫入呢?
答案是:不一定能看到。
在單線程程序中,對(duì)存在控制依賴的操作重排序,不會(huì)改變執(zhí)行結(jié)果(這也是as-if-serial語(yǔ)義允許對(duì)存在控制依賴的操作做重排序的原因);但在多線程程序中,對(duì)存在控制依賴的操作重排序,可能會(huì)改變程序的執(zhí)行結(jié)果。
順序一致性
順序一致性內(nèi)存模型是一個(gè)理論參考模型,在設(shè)計(jì)的時(shí)候,處理器的內(nèi)存模型和編程語(yǔ)言的內(nèi)存模型都會(huì)以順序一致性內(nèi)存模型作為參照。
數(shù)據(jù)競(jìng)爭(zhēng)與順序一致性
當(dāng)程序未正確同步時(shí),就可能會(huì)存在數(shù)據(jù)競(jìng)爭(zhēng)。Java內(nèi)存模型規(guī)范對(duì)數(shù)據(jù)競(jìng)爭(zhēng)的定義如下。
在一個(gè)線程中寫一個(gè)變量,在另一個(gè)線程讀同一個(gè)變量,而且寫和讀沒(méi)有通過(guò)同步來(lái)排序。
當(dāng)代碼中包含數(shù)據(jù)競(jìng)爭(zhēng)時(shí),程序的執(zhí)行往往產(chǎn)生違反直覺(jué)的結(jié)果。如果一個(gè)多線程程序能正確步,這個(gè)程序?qū)⑹且粋€(gè)沒(méi)有數(shù)據(jù)競(jìng)爭(zhēng)的程序。
JMM對(duì)正確同步的多線程程序的內(nèi)存一致性做了如下保證。
如果程序是正確同步的,程序的執(zhí)行將具有順序一致性(Sequentially Consistent)——即程序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果相同。馬上我們就會(huì)看到,這對(duì)于程序員來(lái)說(shuō)是一個(gè)極強(qiáng)的保證。這里的同步是指廣義上的同步,包括對(duì)常用同步原語(yǔ)(synchronized、volatile和final)的正確使用。
順序一致性內(nèi)存模型
順序一致性內(nèi)存模型是一個(gè)被計(jì)算機(jī)科學(xué)家理想化了的理論參考模型,它為程序員提供了極強(qiáng)的內(nèi)存可見(jiàn)性保證。順序一致性內(nèi)存模型有兩大特性。
1)一個(gè)線程中的所有操作必須按照程序的順序來(lái)執(zhí)行。
2)(不管程序是否同步)所有線程都只能看到一個(gè)單一的操作執(zhí)行順序。在順序一致性內(nèi)存模型中,每個(gè)操作都必須原子執(zhí)行且立刻對(duì)所有線程可見(jiàn)。
假設(shè)有兩個(gè)線程A和B并發(fā)執(zhí)行。其中A線程有3個(gè)操作,它們?cè)诔绦蛑械捻樞蚴牵篈1→A2→A3。B線程也有3個(gè)操作,它們?cè)诔绦蛑械捻樞蚴牵築1→B2→B3。
假設(shè)這兩個(gè)線程使用監(jiān)視器鎖來(lái)正確同步:A線程的3個(gè)操作執(zhí)行后釋放監(jiān)視器鎖,隨后B線程獲取同一個(gè)監(jiān)視器鎖。那么程序在順序一致性模型中的執(zhí)行效果將如圖3-11所示。
現(xiàn)在我們?cè)偌僭O(shè)這兩個(gè)線程沒(méi)有做同步,下面是這個(gè)未同步程序在順序一致性模型中的執(zhí)行示意圖,如圖3-12所示。
未同步程序在順序一致性模型中雖然整體執(zhí)行順序是無(wú)序的,但所有線程都只能看到一個(gè)一致的整體執(zhí)行順序。以上圖為例,線程A和B看到的執(zhí)行順序都是:B1→A1→A2→B2→A3→B3。之所以能得到這個(gè)保證是因?yàn)轫樞蛞恢滦詢?nèi)存模型中的每個(gè)操作必須立即對(duì)任意線程可見(jiàn)。
但是,在JMM中就沒(méi)有這個(gè)保證。未同步程序在JMM中不但整體的執(zhí)行順序是無(wú)序的,而且所有線程看到的操作執(zhí)行順序也可能不一致。比如,在當(dāng)前線程把寫過(guò)的數(shù)據(jù)緩存在本地內(nèi)存中,在沒(méi)有刷新到主內(nèi)存之前,這個(gè)寫操作僅對(duì)當(dāng)前線程可見(jiàn);從其他線程的角度來(lái)觀察,會(huì)認(rèn)為這個(gè)寫操作根本沒(méi)有被當(dāng)前線程執(zhí)行。只有當(dāng)前線程把本地內(nèi)存中寫過(guò)的數(shù)據(jù)刷新到主內(nèi)存之后,這個(gè)寫操作才能對(duì)其他線程可見(jiàn)。在這種情況下,當(dāng)前線程和其他線程看到的操作執(zhí)行順序?qū)⒉灰恢隆?/p>
同步程序的順序一致性效果
class SynchronizedExample { int a = 0; boolean flag = false; public synchronized void writer() { // 獲取鎖 a = 1; flag = true; } // 釋放鎖 public synchronized void reader() { // 獲取鎖 if (flag) { int i = a; …… } // 釋放鎖 } }
順序一致性模型中,所有操作完全按程序的順序串行執(zhí)行。而在JMM中,臨界區(qū)內(nèi)的代碼可以重排序(但JMM不允許臨界區(qū)內(nèi)的代碼“逸出”到臨界區(qū)之外,那樣會(huì)破壞監(jiān)視器的語(yǔ)義)。JMM會(huì)在退出臨界區(qū)和進(jìn)入臨界區(qū)這兩個(gè)關(guān)鍵時(shí)間點(diǎn)做一些特別處理,使得線程在這兩個(gè)時(shí)間點(diǎn)具有與順序一致性模型相同的內(nèi)存視圖(具體細(xì)節(jié)后文會(huì)說(shuō)明)。雖然線程A在臨界區(qū)內(nèi)做了重排序,但由于監(jiān)視器互斥執(zhí)行的特性,這里的線程B根本無(wú)法“觀察”到線程A在臨界區(qū)內(nèi)的重排序。這種重排序既提高了執(zhí)行效率,又沒(méi)有改變程序的執(zhí)行結(jié)果。
從這里我們可以看到,JMM在具體實(shí)現(xiàn)上的基本方針為:在不改變(正確同步的)程序執(zhí)行結(jié)果的前提下,盡可能地為編譯器和處理器的優(yōu)化打開方便之門。
JMM不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致。因?yàn)槿绻胍WC執(zhí)行結(jié)果一致,JMM需要禁止大量的處理器和編譯器的優(yōu)化,這對(duì)程序的執(zhí)行性能會(huì)產(chǎn)生很大的影響。
未同步程序在兩個(gè)模型中的執(zhí)行特性有如下幾個(gè)差異。
1)順序一致性模型保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行,而JMM不保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行(比如上面正確同步的多線程程序在臨界區(qū)內(nèi)的重排序)
2)順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序,而JMM不保證所有線程能看到一致的操作執(zhí)行順序。
3)JMM不保證對(duì)64位的long型和double型變量的寫操作具有原子性,而順序一致性模型保證對(duì)所有的內(nèi)存讀/寫操作都具有原子性。
第3個(gè)差異與處理器總線的工作機(jī)制密切相關(guān)。在計(jì)算機(jī)中,數(shù)據(jù)通過(guò)總線在處理器和內(nèi)存之間傳遞。每次處理器和內(nèi)存之間的數(shù)據(jù)傳遞都是通過(guò)一系列步驟來(lái)完成的,這一系列步驟稱之為總線事務(wù)(Bus Transaction)。總線事務(wù)包括讀事務(wù)(Read Transaction)和寫事務(wù)(Write Transaction)。讀事務(wù)從內(nèi)存?zhèn)魉蛿?shù)據(jù)到處理器,寫事務(wù)從處理器傳送數(shù)據(jù)到內(nèi)存,每個(gè)事務(wù)會(huì)讀/寫內(nèi)存中一個(gè)或多個(gè)物理上連續(xù)的字。
在一些32位的處理器上,如果要求對(duì)64位數(shù)據(jù)的寫操作具有原子性,會(huì)有比較大的開銷。為了照顧這種處理器,Java語(yǔ)言規(guī)范鼓勵(lì)但不強(qiáng)求JVM對(duì)64位的long型變量和double型變量的寫操作具有原子性。當(dāng)JVM在這種處理器上運(yùn)行時(shí),可能會(huì)把一個(gè)64位long/double型變量的寫操作拆分為兩個(gè)32位的寫操作來(lái)執(zhí)行。這兩個(gè)32位的寫操作可能會(huì)被分配到不同的總線事務(wù)中執(zhí)行,此時(shí)對(duì)這個(gè)64位變量的寫操作將不具有原子性。
注意,在JSR-133之前的舊內(nèi)存模型中,一個(gè)64位long/double型變量的讀/寫操作可以被拆分為兩個(gè)32位的讀/寫操作來(lái)執(zhí)行。從JSR-133內(nèi)存模型開始(即從JDK5開始),僅僅只允許把一個(gè)64位long/double型變量的寫操作拆分為兩個(gè)32位的寫操作來(lái)執(zhí)行,任意的讀操作在JSR-133中都必須具有原子性(即任意讀操作必須要在單個(gè)讀事務(wù)中執(zhí)行)。
volatile的內(nèi)存語(yǔ)義
當(dāng)聲明共享變量為volatile后,對(duì)這個(gè)變量的讀/寫將會(huì)很特別。為了揭開volatile的神秘面紗,下面將介紹volatile的內(nèi)存語(yǔ)義及volatile內(nèi)存語(yǔ)義的實(shí)現(xiàn)。
鎖的語(yǔ)義決定了臨界區(qū)代碼的執(zhí)行具有原子性。這意味著,即使是64位的long型和double型變量,只要它是volatile變量,對(duì)該變量的讀/寫就具有原子性。如果是多個(gè)volatile操作或類似于volatile++這種復(fù)合操作,這些操作整體上不具有原子性。
簡(jiǎn)而言之,volatile變量自身具有下列特性。
可見(jiàn)性。對(duì)一個(gè)volatile變量的讀,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫入。
原子性。對(duì)任意單個(gè)volatile變量的讀/寫具有原子性,但類似于volatile++這種復(fù)合操不具有原子性。
volatile內(nèi)存語(yǔ)義的實(shí)現(xiàn)
重排序分為編譯器重排序和處理器重排序。為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義,JMM會(huì)分別限制這兩種類型的重排序類型。
當(dāng)?shù)诙€(gè)操作是volatile寫時(shí),不管第一個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后。
當(dāng)?shù)谝粋€(gè)操作是volatile讀時(shí),不管第二個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前。
當(dāng)?shù)谝粋€(gè)操作是volatile寫,第二個(gè)操作是volatile讀時(shí),不能重排序。
為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類型的處理器重排序。對(duì)于編譯器來(lái)說(shuō),發(fā)現(xiàn)一個(gè)最優(yōu)布置來(lái)最小化插入屏障的總數(shù)幾乎不可能。為此,JMM采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略。
在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
上述內(nèi)存屏障插入策略非常保守,但它可以保證在任意處理器平臺(tái),任意的程序中都能得到正確的volatile內(nèi)存語(yǔ)義。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/68483.html
摘要:相比與其他操作系統(tǒng)包括其他類系統(tǒng)有很多的優(yōu)點(diǎn),其中有一項(xiàng)就是,其上下文切換和模式切換的時(shí)間消耗非常少。因?yàn)槎嗑€程競(jìng)爭(zhēng)鎖時(shí)會(huì)引起上下文切換。減少線程的使用。很多編程語(yǔ)言中都有協(xié)程。所以如何避免死鎖的產(chǎn)生,在我們使用并發(fā)編程時(shí)至關(guān)重要。 系列文章傳送門: Java多線程學(xué)習(xí)(一)Java多線程入門 Java多線程學(xué)習(xí)(二)synchronized關(guān)鍵字(1) java多線程學(xué)習(xí)(二)syn...
摘要:因?yàn)槎嗑€程競(jìng)爭(zhēng)鎖時(shí)會(huì)引起上下文切換。減少線程的使用。舉個(gè)例子如果說(shuō)服務(wù)器的帶寬只有,某個(gè)資源的下載速度是,系統(tǒng)啟動(dòng)個(gè)線程下載該資源并不會(huì)導(dǎo)致下載速度編程,所以在并發(fā)編程時(shí),需要考慮這些資源的限制。 最近私下做一項(xiàng)目,一bug幾日未解決,總惶恐。一日頓悟,bug不可怕,怕的是項(xiàng)目不存在bug,與其懼怕,何不與其剛正面。 系列文章傳送門: Java多線程學(xué)習(xí)(一)Java多線程入門 Jav...
摘要:開始學(xué)習(xí)也有一段時(shí)間了,一些基礎(chǔ)的書也掃了一遍了。最近慢慢開始看和,后者的話和有類似之處,都是一些編程經(jīng)驗(yàn)的編程的世界里好多的東西都是相同的。這里其實(shí)是對(duì)的最佳實(shí)踐,之后該對(duì)象已經(jīng)變成一個(gè)過(guò)期的引用了,此時(shí)就應(yīng)該清空這個(gè)引用。 開始學(xué)習(xí)java也有一段時(shí)間了,一些基礎(chǔ)的書也掃了一遍了(think in java/core java volume 1)。最近慢慢開始看和,后者的話和有類似...
摘要:學(xué)習(xí)編程的本最佳書籍這些書涵蓋了各個(gè)領(lǐng)域,包括核心基礎(chǔ)知識(shí),集合框架,多線程和并發(fā),內(nèi)部和性能調(diào)優(yōu),設(shè)計(jì)模式等。擅長(zhǎng)解釋錯(cuò)誤及錯(cuò)誤的原因以及如何解決簡(jiǎn)而言之,這是學(xué)習(xí)中并發(fā)和多線程的最佳書籍之一。 showImg(https://segmentfault.com/img/remote/1460000018913016); 來(lái)源 | 愿碼(ChainDesk.CN)內(nèi)容編輯 愿碼Slo...
摘要:表示的是兩個(gè),當(dāng)其中任意一個(gè)計(jì)算完并發(fā)編程之是線程安全并且高效的,在并發(fā)編程中經(jīng)??梢?jiàn)它的使用,在開始分析它的高并發(fā)實(shí)現(xiàn)機(jī)制前,先講講廢話,看看它是如何被引入的。電商秒殺和搶購(gòu),是兩個(gè)比較典型的互聯(lián)網(wǎng)高并發(fā)場(chǎng)景。 干貨:深度剖析分布式搜索引擎設(shè)計(jì) 分布式,高可用,和機(jī)器學(xué)習(xí)一樣,最近幾年被提及得最多的名詞,聽(tīng)名字多牛逼,來(lái),我們一步一步來(lái)?yè)羝魄皟蓚€(gè)名詞,今天我們首先來(lái)說(shuō)說(shuō)分布式。 探究...
閱讀 476·2021-10-09 09:57
閱讀 483·2019-08-29 18:39
閱讀 820·2019-08-29 12:27
閱讀 3036·2019-08-26 11:38
閱讀 2674·2019-08-26 11:37
閱讀 1300·2019-08-26 10:59
閱讀 1387·2019-08-26 10:58
閱讀 996·2019-08-26 10:48