Java程序執(zhí)行:Java代碼→Java字節(jié)碼→字節(jié)碼被類(lèi)加載器加載到JVM里,JVM執(zhí)行字節(jié)碼→轉(zhuǎn)化為匯編指令在CPU上執(zhí)行。
Java中使用的并發(fā)機(jī)制依賴(lài)于JVM的實(shí)現(xiàn)和CPU的指令,本章我們來(lái)探索下Java并發(fā)機(jī)制的底層原理。
volatile的實(shí)現(xiàn)原理與應(yīng)用
在多線(xiàn)程的并發(fā)編程中synchronized和volatile都扮演著重要的角色,volatile是輕量級(jí)的synchronized,它在處理器開(kāi)發(fā)中保證了共享變量的“可見(jiàn)性”(可見(jiàn)性:當(dāng)一個(gè)線(xiàn)程修改一個(gè)共享變量時(shí),另外一個(gè)線(xiàn)程能讀到這個(gè)修改的值。)。
如果volatile變量修飾符使用恰當(dāng)?shù)脑?huà),它比synchronized的使用和執(zhí)行成本更低,因?yàn)樗?strong>不會(huì)引起上下文的切換和調(diào)度。
Java語(yǔ)言規(guī)范第3版中對(duì)volatile的定義如下:Java編程語(yǔ)言允許線(xiàn)程訪(fǎng)問(wèn)共享變量,為了確保共享變量能被準(zhǔn)確和一致的更新,線(xiàn)程應(yīng)該確保通過(guò)排他鎖單獨(dú)獲得這個(gè)變量。
如果一個(gè)字段被聲明稱(chēng)volatile,Java線(xiàn)程內(nèi)存模型確保所線(xiàn)程成看到這個(gè)變量的值是一致的。
volatile如何來(lái)保證可見(jiàn)性呢?
Java代碼:instance = new Singleton() //instance是volatile變量
轉(zhuǎn)為匯編代碼:0x01a3de1d: movb $0 ? 0, 0 ? 1104800(%esi);0x01a3de24: lock addl $0 ? 0,(%esp);
有volatile變量修飾的共享變量進(jìn)行寫(xiě)操作的時(shí)候會(huì)多處第二行匯編代碼,關(guān)鍵在與Lock指令,Lock前綴的指令在多核處理器下會(huì)引起兩件事情:
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫(xiě)回到系統(tǒng)內(nèi)存。
- 這個(gè)寫(xiě)回內(nèi)存的操作會(huì)使其它CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效。
詳細(xì)說(shuō)明:為了提高處理速度,處理器不直接和內(nèi)存進(jìn)行通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存后再進(jìn)行操作,但操作完不知道何時(shí)會(huì)寫(xiě)到內(nèi)存。如果對(duì)聲明了volatile的變量進(jìn)行寫(xiě)操作,JVM就會(huì)向處理器發(fā)送一條Lock前綴的指令,將這個(gè)變量所在緩存行的數(shù)據(jù)寫(xiě)回到系統(tǒng)內(nèi)存。但是,就算寫(xiě)回到內(nèi)存,如果其他處理器緩存的值還是舊的,再執(zhí)行計(jì)算操作還是會(huì)有問(wèn)題。所以,在多處理器下,為了保證各個(gè)處理器的緩存是一致的,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議,每個(gè)處理器通過(guò)嗅探在總線(xiàn)上傳播的數(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ù)讀到處理器緩存里。
synchronized的實(shí)現(xiàn)原理與應(yīng)用
在多線(xiàn)程并發(fā)編程中synchronized一直是元老級(jí)角色,被稱(chēng)為重量級(jí)鎖。但是,隨著Java SE 1.6對(duì)其進(jìn)行各種優(yōu)化,使得它在有些時(shí)候不是那么重。
利用synchronized實(shí)現(xiàn)同步的基礎(chǔ):Java的每一個(gè)對(duì)象都可以作為鎖。
- 對(duì)于普通同步方法,鎖是當(dāng)前實(shí)例對(duì)象。
- 對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類(lèi)的Class對(duì)象。
- 對(duì)于同步方法塊,鎖是synchronized括號(hào)里配置的對(duì)象。
Java對(duì)象頭(暫時(shí)不細(xì)說(shuō)此概念)
synchronized用的鎖是存在Java對(duì)象頭里的。
鎖的升級(jí)與對(duì)比
Java SE 1.6為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗,引入了“偏向鎖”和“輕量級(jí)鎖”,在Java SE 1.6中,鎖一共有四種狀態(tài),級(jí)別從低到高一次:無(wú)鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)、重量級(jí)鎖狀態(tài)。
這幾個(gè)鎖會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí),鎖可以升級(jí)但不可以降級(jí)。這樣做的目的是為了提高獲得鎖和釋放鎖的效率。
| 鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
|---|---|---|---|
| 偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距 | 如果線(xiàn)程間存在鎖競(jìng)爭(zhēng),會(huì)帶來(lái)額外的鎖撤銷(xiāo)的消耗 | 適用于只有一個(gè)線(xiàn)程訪(fǎng)問(wèn)同步快場(chǎng)景 |
| 輕量級(jí)鎖 | 競(jìng)爭(zhēng)的線(xiàn)程不會(huì)阻塞,提高程序的響應(yīng)速度 | 如果始終得不到鎖競(jìng)爭(zhēng)的線(xiàn)程,使用自旋會(huì)消耗CPU | 追求響應(yīng)時(shí)間,同步塊執(zhí)行速度非???/td> |
| 重量級(jí)鎖 | 線(xiàn)程競(jìng)爭(zhēng)不使用自旋,不會(huì)消耗CPU | 線(xiàn)程阻塞,響應(yīng)時(shí)間緩慢 | 追求吞吐量,同步塊執(zhí)行速度較長(zhǎng) |
原子操作的實(shí)現(xiàn)原理(大致聊一聊)
原子操作:不可被中斷的一個(gè)或一系列操作。
處理器如何實(shí)現(xiàn)原子操作:
- 使用總線(xiàn)鎖保證原子性。
- 使用緩存鎖定保證原子性。
但是有兩種特殊情況下處理器不會(huì)使用緩存鎖定:
- 當(dāng)操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部,或操作的數(shù)據(jù)跨多個(gè)緩存行時(shí),則處理器會(huì)調(diào)用總線(xiàn)鎖定。
- 有些處理器不支持緩存鎖定。
Java如何實(shí)現(xiàn)原子操作:
使用鎖和循環(huán)CAS的方式實(shí)現(xiàn)原子操作。
CAS實(shí)現(xiàn)原子操作的三大問(wèn)題:
- ABA問(wèn)題。
- 循環(huán)時(shí)間長(zhǎng)開(kāi)銷(xiāo)大。
- 只能保證一個(gè)共享變量的原子操作。
結(jié)語(yǔ):本篇Tomato主要了解下,volatile、synchronized、原子操作的原理,其中原子操作部分說(shuō)明的不是很詳細(xì),因?yàn)門(mén)omato自己還沒(méi)有研究明白,之后會(huì)進(jìn)行更新的。