在解釋【偽共享】這個(gè)概念之前争拐,我們先來(lái)運(yùn)行一段代碼康嘉,小編的電腦上有4個(gè)core仆抵。
這個(gè)程序的邏輯是4個(gè)線程共享同一個(gè)數(shù)組讀寫不同下標(biāo)的變量。每個(gè)線程循環(huán)1億次讀寫密浑,也就是+1操作蛙婴。然后統(tǒng)計(jì)4個(gè)線程同時(shí)跑完總共花的時(shí)間。
下面我們來(lái)看看在小編的電腦上運(yùn)行的結(jié)果
然后我把SharingLong里面的注釋代碼去掉尔破,再跑了一下街图。
在性能上注釋前后差別高達(dá)5比1,為什么會(huì)在性能上會(huì)產(chǎn)生如此大的差別呢懒构?
這就是本篇要將的主題【偽共享】餐济,英文名叫False Sharing。而SharingLong里面的注釋行一般稱之為【緩存行填充】胆剧,英文名叫Cache Line Padding絮姆。
首先我們來(lái)計(jì)算一下SharingLong對(duì)象占用的內(nèi)存空間,我們不考慮64位的情景赞赖,Java的對(duì)象都有一個(gè)2個(gè)word的頭部滚朵,第一個(gè)word存儲(chǔ)對(duì)象的hashcode和一些特殊的位標(biāo)志,如GC的分代年齡前域、偏向鎖標(biāo)記等辕近,第二個(gè)word存儲(chǔ)對(duì)象的指針地址,一個(gè)word就是32位匿垄。然后加上v和6個(gè)p變量移宅,總共就是8個(gè)long的長(zhǎng)度,也就是64字節(jié)椿疗。
接下來(lái)我們要引入CPU緩存的概念漏峰。
現(xiàn)代的處理器一般都有3級(jí)緩存結(jié)構(gòu),L1届榄、L2和L3浅乔,CPU直接訪問(wèn)主存是一個(gè)相對(duì)比較慢的操作,所以通過(guò)3級(jí)緩存來(lái)提升訪存性能。我們將3個(gè)緩存當(dāng)成一個(gè)整體來(lái)看待靖苇,它就是CPU緩存席噩。緩存的制造成本非常昂貴,它一般要比主存空間小的多贤壁。
CPU在讀主存的時(shí)候悼枢,會(huì)先將主存的一塊數(shù)據(jù)加載到緩存上,然后在緩存上讀取脾拆。當(dāng)CPU寫主存的時(shí)候馒索,它會(huì)首先寫緩存,在未來(lái)的某個(gè)時(shí)間點(diǎn)再一次性將緩存的數(shù)據(jù)全部刷回主存名船,這樣就可以提高寫操作的性能绰上。因?yàn)橛?jì)算機(jī)程序數(shù)據(jù)操作的局部性,CPU連續(xù)的指令傾向于訪問(wèn)相鄰地址空間的數(shù)據(jù)包帚,所以后續(xù)的讀寫操作有很大的概率可以直接在緩存上拿到數(shù)據(jù)渔期。如果緩存上不存在运吓,那就再去主存上加載進(jìn)來(lái)渴邦。
緩存雖然小,但是也不是太小拘哨,CPU在加載主存數(shù)據(jù)時(shí)谋梭,如果一次性將整個(gè)Cache填滿,但是接下來(lái)的指令訪問(wèn)的數(shù)據(jù)又不在緩存上倦青,就會(huì)導(dǎo)致讀浪費(fèi)瓮床。另外如果只修改了其中幾個(gè)字節(jié)的數(shù)據(jù),但是得回寫整個(gè)Cache到內(nèi)存产镐,這又會(huì)導(dǎo)致寫浪費(fèi)隘庄。
所以現(xiàn)代的CPU緩存一般是分行存儲(chǔ)的,最小處理單位是一個(gè)行癣亚,這個(gè)行的長(zhǎng)度一般來(lái)說(shuō)就是上文提到的64字節(jié)丑掺,我們稱之為【緩存行】。
SharingLong對(duì)象中v的值是volatile類型的述雾,意味著CPU要保證v變量在不同線程之間的讀寫可見(jiàn)行街州。當(dāng)CPU對(duì)v變量進(jìn)行修改的時(shí)候會(huì)將數(shù)據(jù)立即回寫至主存并將相應(yīng)的緩存行置為失效。這樣后續(xù)對(duì)v變量進(jìn)行的讀寫操作都需要重新從內(nèi)存中加載緩存行玻孟,這樣就保證了其它線程讀到的數(shù)據(jù)是最新的唆缴。
這點(diǎn)跟我們平常在Java基礎(chǔ)教科書(shū)里提到的有點(diǎn)不一樣。教科書(shū)里面為了便于新手理解黍翎,不會(huì)提及緩存面徽,一般只會(huì)說(shuō)volatile變量直接讀寫內(nèi)存。
如果內(nèi)存里有兩個(gè)volatile變量在相鄰的地址匣掸,兩個(gè)cpu分別對(duì)v1和v2進(jìn)行讀和寫操作趟紊,會(huì)發(fā)生什么情況呢质礼?首先我們分解執(zhí)行動(dòng)作。圖中的h表示對(duì)象頭织阳。
CPU1對(duì)v1進(jìn)行讀操作眶蕉,將內(nèi)存里的v1加載到緩存行里。
CPU2對(duì)v2進(jìn)行讀操作唧躲,將內(nèi)存里的v2加載到緩存行里造挽。
CPU1對(duì)v1進(jìn)行寫操作,將緩存里的v1修改弄痹,然后回寫到主存再將緩存行置為失效饭入。
CPU2對(duì)v2進(jìn)行寫操作,將緩存里的v2修改肛真,然后回寫到主存再將緩存行置為失效谐丢。
步驟1肯定先于步驟3,步驟2肯定先于步驟4蚓让。它們發(fā)生的順序可能是 1->2->3->4 乾忱,相當(dāng)于兩個(gè)CPU交疊運(yùn)行,步驟1加載緩存行历极,步驟2發(fā)現(xiàn)數(shù)據(jù)就在緩存行里還是最新的窄瘟,就省去了加載緩存行操作了,這時(shí)讀操作做到了【共享】趟卸。緊接著步驟3正常進(jìn)行寫操作蹄葱,然后步驟4來(lái)了,CPU2發(fā)現(xiàn)緩存行失效了锄列,所以還得重新加載緩存行图云,然后再回寫到主存再將緩存行置為失效。這里就發(fā)生了重復(fù)加載緩存行的現(xiàn)象邻邮,也即【寫競(jìng)爭(zhēng)】竣况。如果不是volatile變量,步驟3的寫操作是不會(huì)立即回寫內(nèi)存的饶囚,緩存行也就不會(huì)立即置為失效帕翻,這個(gè)時(shí)候步驟4來(lái)了CPU可以直接對(duì)緩存進(jìn)行寫操作,而不會(huì)出現(xiàn)浪費(fèi)現(xiàn)象萝风。我們稱這種現(xiàn)象為【偽共享】嘀掸,就是說(shuō)這兩個(gè)變量雖然共享同一個(gè)緩存行,但是它們之間會(huì)發(fā)生寫競(jìng)爭(zhēng)规惰。
如果順序是1->3->2->4睬塌,步驟1和步驟3的讀操作這時(shí)就沒(méi)能實(shí)現(xiàn)共享,還是會(huì)有浪費(fèi)。
當(dāng)系統(tǒng)的線程數(shù)越多時(shí)揩晴,寫競(jìng)爭(zhēng)越激烈勋陪,這種浪費(fèi)就越多。
現(xiàn)在我們能明白為什么去掉注釋后硫兰,程序會(huì)變慢诅愚,因?yàn)榇嬖趯懜?jìng)爭(zhēng)現(xiàn)象,數(shù)組中相鄰的SharingLong.v共享了同一個(gè)緩存行劫映。
那加上p1~p6這6個(gè)變量的意義是什么呢违孝?我們看圖。
我們發(fā)現(xiàn)加上6個(gè)long變量后泳赋,v1和v2將分別占用自己的緩存行雌桑,互不干擾,所以寫競(jìng)爭(zhēng)也就不存在了祖今,效率自然就提升了校坑。
不過(guò)缺點(diǎn)也是有的,就是緩存的利用率降低了千诬,一個(gè)緩存行的空間才使用了1/4耍目。這就是典型的空間換時(shí)間的場(chǎng)景。
例子中我們使用了volatile變量大渤,那如果改成普通變量呢制妄?我們運(yùn)行一下,結(jié)果如下泵三。
相當(dāng)驚人,耗時(shí)上居然少了3個(gè)量級(jí)衔掸,這就是volatile在性能上的代價(jià)烫幕。普通變量不需要保證線程之間的讀寫的可見(jiàn)性,CPU對(duì)緩存修改后不需要立即回寫內(nèi)存敞映,不存在寫操作緩存穿透現(xiàn)象较曼。而讀操作也不需要總是重新從內(nèi)存加載,那這個(gè)效率幾乎完全就是緩存訪問(wèn)的效率振愿,而對(duì)volatile變量的讀寫操作則接近內(nèi)存訪問(wèn)的效率捷犹,差距自然如此明顯。
你也許會(huì)問(wèn)冕末,知道這些有什么蛋用萍歉!
確是沒(méi)什么蛋用割岛,因?yàn)樵诂F(xiàn)實(shí)世界譬涡,大部分操作都涉及到IO操作寂殉。根據(jù)水桶效應(yīng),其它環(huán)節(jié)優(yōu)化到了極致租副,也無(wú)法提升整體的質(zhì)量。
但是也不完全所有的應(yīng)用都是IO操作型的皆串,有一些場(chǎng)景下那是純粹的內(nèi)存操作殷费。那么對(duì)于純內(nèi)存操作來(lái)說(shuō),理解【偽共享】知識(shí)可以幫你從性能上提升幾倍甚至是幾個(gè)數(shù)量級(jí)攻询。
著名的disruptor框架正是使用了緩存行填充技術(shù)从撼,才使得它的環(huán)形數(shù)組隊(duì)列能如此高效【埽看wiki上的性能報(bào)告谋逻,disruptor的RingBuffer相比Java內(nèi)置的ArrayBlockingQueue在OPS上高出近一個(gè)數(shù)量級(jí),在隊(duì)列延遲上則低了接近3個(gè)數(shù)量級(jí)桐经。
閱讀相關(guān)文章毁兆,關(guān)注微信公眾號(hào)/知乎專欄/頭條號(hào)【碼洞】