java虚拟机线程调优与底层原理分析_Java并发编程——多线程的底层原理
“Java代碼在編譯后會(huì)變成Java字節(jié)碼,字節(jié)碼被類加載器加載到JVM里,JVM執(zhí)行字節(jié)碼,最終需要轉(zhuǎn)化為匯編指令在CPU上執(zhí)行,Java中所使用的并發(fā)機(jī)制依賴于JVM的實(shí)現(xiàn)和 CPU的指令。本章我們將深入底層一起探索下Java并發(fā)機(jī)制的底層實(shí)現(xiàn)原理
——java并發(fā)編程的藝術(shù)
”
volatile關(guān)鍵字
并發(fā)編程中synchronized和volatile都扮演著重要的角色,volatile是輕量級(jí)的synchronized,他在多線程的開發(fā)中保證了共享變量的內(nèi)存可見(jiàn)性。可見(jiàn)性是指,一個(gè)線程在修改一個(gè)共享變量時(shí),另外一個(gè)線程能讀到這個(gè)修改的值,而不是內(nèi)存或者CPU緩存的值。volatile不會(huì)引起線程上下文的切換和調(diào)度。
volatile的定義與實(shí)現(xiàn)原理
定義
java語(yǔ)言規(guī)范第三版對(duì)volatile的定義如下:Java編程語(yǔ)言允許線程訪問(wèn)共享變量,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過(guò)排他鎖單獨(dú)獲得這個(gè)變量。Java語(yǔ)言 提供了volatile,在某些情況下比鎖要更加方便。如果一個(gè)字段被聲明成volatile,Java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的。
volatile有關(guān)的CPU術(shù)語(yǔ)
在了解volatile的原理之前,我們需要看下與其實(shí)現(xiàn)原理相關(guān)的CPU術(shù)語(yǔ)與說(shuō)明
| 術(shù)語(yǔ) | 英文單詞 | 術(shù)語(yǔ)描述 |
|---|---|---|
| 內(nèi)存屏障 | memory barriers | 是一組處理器指令,用于實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制 |
| 緩沖行 | cache line | 緩存中最小的存儲(chǔ)單位,處理器填寫緩存線時(shí)會(huì)加載整個(gè)緩存線,需要使用多個(gè)主內(nèi)存讀周期(偽共享的關(guān)鍵原因) |
| 原子操作 | atomic operations | 不可中斷的一個(gè)或一系列操作 |
| 緩存行填充 | cache line fill | 當(dāng)處理器識(shí)別到從內(nèi)存中讀取操作數(shù)是可緩存的,處理器讀取整個(gè)緩存行到適當(dāng)?shù)木彺?L1,L2,L3或所有) |
| 緩存命中 | cache hit | 如果進(jìn)行高速緩存行填充操作的內(nèi)存位置仍然是下次處理器訪問(wèn)的地址時(shí),處理器從緩存中讀取操作數(shù),而不是從內(nèi)存中讀取(讀操作) |
| 寫命中 | write hit | 當(dāng)處理器將操作數(shù)寫回到一個(gè)內(nèi)存緩存的區(qū)域時(shí),他首先會(huì)檢查這個(gè)緩存的內(nèi)存地址是否在緩存行中,如果存在一個(gè)有效的緩存行,則處理器將這個(gè)操作數(shù)寫回緩存,而不是寫回到內(nèi)存,這個(gè)操作被稱為寫命中(寫操作) |
| 寫缺失 | write misses the cache | 一個(gè)有效的緩存行被寫入到不存在的內(nèi)存區(qū)域(寫之前緩沖區(qū)沒(méi)有操作數(shù)的緩存) |
那么volatile是如何保證內(nèi)存可見(jiàn)性的呢?我們?cè)赬86處理器下通過(guò)工具獲取到JIT編譯器生成的匯編指令來(lái)查看對(duì)volatile進(jìn)行寫操作,CPU會(huì)做什么事情
Java代碼如下:
instance?=?new?Singleton()??????????????????//instance是volatile變量
轉(zhuǎn)變成匯編代碼,如下:
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
有volatile關(guān)鍵字修飾的共享變量進(jìn)行寫操作的時(shí)候會(huì)多出第二行匯編代碼,Lock前綴的指令在多核處理器會(huì)引發(fā)兩件事情:
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
- 這個(gè)寫回內(nèi)存的操作會(huì)使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效
這就是Lock指令保證了CPU緩存的一致性。當(dāng)處理器對(duì)被volatile關(guān)鍵字修飾的變量修改時(shí),會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。
volatile的實(shí)現(xiàn)原則
- Lock前綴指令會(huì)引起處理器緩存寫回到內(nèi)存
- 一個(gè)處理器的緩存寫到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存無(wú)效
volatile的使用優(yōu)化
追加字節(jié)優(yōu)化性能
因?yàn)閷?duì)于Intel core i7、酷睿、Atom和NetBurst,以及Core Solo和Pentium M處理器的L1、L2、L3的高速緩存行都是64字節(jié)寬,不支持部分填充緩存行,這意味著,如果隊(duì)列的頭節(jié)點(diǎn)和尾節(jié)點(diǎn)都不足64字節(jié)的話、處理器會(huì)將他們讀到同一個(gè)高速緩存行中,在多處理器下每個(gè)處理器都會(huì)緩存同樣的頭、尾節(jié)點(diǎn),當(dāng)一個(gè)處理器試圖修改頭節(jié)點(diǎn)的時(shí)候,會(huì)將整個(gè)緩存行鎖定,導(dǎo)致其他處理器無(wú)法訪問(wèn)自己高速緩存中的尾節(jié)點(diǎn),造成了偽共享問(wèn)題。這嚴(yán)重影響了效率。
但是如果我們將頭、尾節(jié)點(diǎn)填充到64字節(jié),處理器就不會(huì)把他們放在同一個(gè)緩存行中,這就解決了偽共享問(wèn)題。
但是,也不是在使用volatile變量時(shí)都應(yīng)該追加到64字節(jié),下面兩種情景就不需要
- 緩存行非64字節(jié)寬的處理器
- 共享變量不會(huì)被頻繁的讀寫
不過(guò)在Java7中,追加字節(jié)的方式可能不生效,因?yàn)镴ava7更加智能,會(huì)淘汰或者重新排列無(wú)用字段,除了volatile,Java并發(fā)編程中應(yīng)用比較多的是synchronized。
synchronized的實(shí)現(xiàn)原理與應(yīng)用
synchronized被稱為重量級(jí)鎖。實(shí)際上隨著Java SE對(duì)synchronized進(jìn)行各種優(yōu)化之后,有些情況他就沒(méi)那么重了。
synchronized實(shí)現(xiàn)同步的基礎(chǔ)
Java中每一個(gè)對(duì)象都可以作為鎖。具體表現(xiàn)為以下三種形式:
- 對(duì)于普通同步方法,鎖是當(dāng)前實(shí)例對(duì)象
- 對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的Class對(duì)象
- 對(duì)于同步方法塊,鎖是synchronized括號(hào)里配置的對(duì)象
當(dāng)一個(gè)線程試圖訪問(wèn)同步代碼塊時(shí),他首先必須得到鎖,退出或拋出異常時(shí)必須釋放鎖。那么,鎖究竟是什么?
JVM基于進(jìn)入和退出Monitor對(duì)象來(lái)實(shí)現(xiàn)方法同步和代碼塊同步,但兩者細(xì)節(jié)不一樣,代碼塊同步是使用monitorenter和monitorexit指令實(shí)現(xiàn)的。而方法同步是使用另外一種方式實(shí)現(xiàn)的,這里不詳細(xì)說(shuō)明。
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ì)象頭里的。如果對(duì)象是數(shù)組類型,則虛擬機(jī)用3個(gè)字寬存儲(chǔ)對(duì)象頭,如果是非數(shù)組類型,則用2字寬存儲(chǔ)對(duì)象頭。
在32位虛擬機(jī)中,1字寬等于4字節(jié),即32bit。
| 長(zhǎng)度 | 內(nèi)容 | 說(shuō)明 |
|---|---|---|
| 32/64bit | Mark Word | 存儲(chǔ)對(duì)象的hashCode或鎖信息等 |
| 32/64bit | Class Metadata Address | 存儲(chǔ)對(duì)象類型數(shù)據(jù)的指針 |
| 32/64bit | Array length | 數(shù)組的長(zhǎng)度(如果當(dāng)前對(duì)象是數(shù)組) |
Java對(duì)象頭里的Mark Word里默認(rèn)存儲(chǔ)對(duì)象的HashCode、分代年齡和所標(biāo)記位。32位JVM的MarkWord的默認(rèn)存儲(chǔ)結(jié)構(gòu)如下表
| 鎖狀態(tài) | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit鎖標(biāo)志位 |
|---|---|---|---|---|
| 無(wú)鎖狀態(tài) | 對(duì)象的hashCode | 對(duì)象分代年齡 | 0 | 01 |
在運(yùn)行期間,Mark Word存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化,其有可能變化為存儲(chǔ)以下4種數(shù)據(jù)。
在64位虛擬機(jī)下,Mark Word是64bit大小的,其存儲(chǔ)結(jié)構(gòu)如下表:
鎖的升級(jí)
為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗,引入了“偏向鎖”和“輕量級(jí)鎖”,在JavaSE 1.6中,鎖一共有4種狀態(tài),從低到高依次是無(wú)鎖狀態(tài),偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài)。鎖可以升級(jí)但不能降級(jí),這是為了提高獲得鎖和釋放鎖的效率。
偏向鎖
在大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。當(dāng)一個(gè)線程訪問(wèn)同步塊并獲取鎖時(shí),會(huì)在對(duì)象頭和棧幀的鎖記錄里存儲(chǔ)鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行CAS操作來(lái)加鎖和解鎖,只需要測(cè)試一下對(duì)象頭的Mark Word里是否儲(chǔ)存著指向當(dāng)前線程的偏向鎖。如果測(cè)試失敗,會(huì)檢測(cè)Mark Word里偏向鎖的標(biāo)識(shí)是否設(shè)置為1(表示當(dāng)前是偏向鎖):如果沒(méi)有設(shè)置,則使用CAS競(jìng)爭(zhēng)鎖;如果設(shè)置了,則嘗試使用CAS將對(duì)象頭的偏向鎖指向當(dāng)前線程。
只有遇到其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)主動(dòng)釋放偏向鎖。線程不會(huì)主動(dòng)釋放偏向鎖。
當(dāng)有線程競(jìng)爭(zhēng)的時(shí)候,偏向鎖會(huì)升級(jí)成輕量級(jí)鎖
偏向鎖的撤銷
偏向鎖使用了一種等到競(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ì)暫停并檢測(cè)擁有偏向鎖的線程,如果線程不處于活動(dòng)狀態(tài),就將對(duì)象頭設(shè)置為無(wú)鎖狀態(tài)。如果線程仍然活著,擁有偏向鎖的棧就會(huì)被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄。對(duì)象頭的Mark Word要么重新偏向其他線程,要么恢復(fù)到無(wú)鎖或者標(biāo)記對(duì)象不適合作為偏向鎖,最后喚醒暫停的線程。
關(guān)閉偏向鎖
偏向鎖在Java6和Java7里是默認(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)。
輕量級(jí)鎖
輕量級(jí)鎖加鎖
線程在執(zhí)行同步代碼塊之前,JVM會(huì)先在當(dāng)前線程中創(chuàng)建用于存儲(chǔ)所記錄的空間并將對(duì)象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displace Mark Word。線程嘗試用CAS將對(duì)象頭中的Mark Word替換成指向鎖記錄的指針。如果成功 ,當(dāng)前線程獲得鎖,如果失敗,則表示其他線程競(jìng)爭(zhēng)鎖,當(dāng)前線程便嘗試自旋來(lái)獲取鎖。
輕量級(jí)鎖解鎖
輕量級(jí)解鎖時(shí),會(huì)使用原子的CAS操作將Displaced Mark Word替換成對(duì)象頭,如果成功,則表示沒(méi)有競(jìng)爭(zhēng)發(fā)生。當(dāng)自選超過(guò)一定次數(shù),表示當(dāng)前鎖存在競(jìng)爭(zhēng),鎖就會(huì)膨脹成重量級(jí)鎖。下圖描述了這一過(guò)程
因?yàn)樽孕龝?huì)消耗CPU,為了避免無(wú)用的自旋(比如鎖的線程被阻塞住了),一旦鎖升級(jí)成重量鎖,就不會(huì)再恢復(fù)到輕量級(jí)鎖了。
鎖的優(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)同步塊情景 |
| 輕量級(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í)行速度較長(zhǎng) |
原子操作的實(shí)現(xiàn)原理
原子(atomic)意為不能進(jìn)行分割的最小粒子,Java中的原子操作也是“不可被中斷的一個(gè)或一系列操作”。接下來(lái)簡(jiǎn)單的說(shuō)一說(shuō)Intel處理器和Java里是如何實(shí)現(xiàn)原子操作的。
1、術(shù)語(yǔ)定義
| 術(shù)語(yǔ)名稱 | 英文 | 解釋 |
|---|---|---|
| 緩存行 | Cache Line | 緩存的最小操作單位 |
| 比較并交換 | Compare and Swap | 即CAS操作,通過(guò)比較操作前的值與期望的值是否一樣,如果一樣,就將舊值換成新值 |
| CPU流水線 | CPU pipeline | CPU流水線的工作方式類似于工業(yè)生產(chǎn)的裝配流水線,在CPU中由5~6個(gè)不同功能的電路單元組成一條指令處理流水線,提高CPU的運(yùn)算速度 |
| 內(nèi)存順序沖突 | Memory order violation | 內(nèi)存順序沖突一般是由假共享引起的,假共享是指多個(gè)CPU同時(shí)修改同一個(gè)緩存行的不同部分而引起其中一個(gè)CPU的操作無(wú)效,當(dāng)出現(xiàn)內(nèi)存順序沖突時(shí),CPU必須清空流水線 |
原子操作的實(shí)現(xiàn)
32位IA-32處理器使用基于對(duì)緩存加鎖或總線加鎖的方式來(lái)實(shí)現(xiàn)多處理器之間的原子操作。首先處理器會(huì)自動(dòng)保證基本的內(nèi)存操作的原子性(從內(nèi)存讀取或者寫入一個(gè)字節(jié)是原子的)。復(fù)雜的操作,處理器無(wú)法保證原子性,因此,處理器提供**總線鎖定和緩存鎖定**兩個(gè)機(jī)制來(lái)保證復(fù)雜內(nèi)存操作的原子性。
總線鎖定保證原子性
如果多個(gè)處理器同時(shí)對(duì)共享變量進(jìn)行讀改寫操作,那么共享變量就會(huì)被多個(gè)處理器同時(shí)進(jìn)行操作,這樣讀改寫操作就不是原子的,操作完共享變量的值會(huì)和期望的不一致,如下圖(兩個(gè)線程同時(shí)對(duì)i進(jìn)行累加)
總線索就是來(lái)解決這個(gè)問(wèn)題的:
所謂總線索就是使用處理器提供一個(gè)LOCK#信號(hào),當(dāng)一個(gè)處理器在總線上輸出此信號(hào)時(shí),其他處理器的對(duì)緩存了該共享變量的緩存行的請(qǐng)求將被阻塞住,那么該處理器可以獨(dú)占共享內(nèi)存。
使用緩存鎖保證原子性
在同一時(shí)刻,我們只需保證對(duì)某個(gè)內(nèi)存地址的操作時(shí)原子性即可,但總線鎖定把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)行,并不需要聲明總線鎖,在Pentium6和目前的處理器中可以使用“緩存鎖定”的方式來(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)榫彺嬉恢滦詴?huì)阻止同時(shí)修改由兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù),當(dāng)其他處理器回寫已被鎖定的緩存行的數(shù)據(jù)時(shí),會(huì)使緩存行無(wú)效。
不能使用緩存鎖定的情況
- 當(dāng)操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部,或操作的數(shù)據(jù)跨多個(gè)緩存行時(shí),則處理器會(huì)調(diào)用總線鎖定
- 有些處理器不支持緩存鎖定。如Intel 486和Pentium處理器,就算鎖定的內(nèi)存區(qū)域在處理器的緩存行中也會(huì)調(diào)用總線鎖定。
Java實(shí)現(xiàn)原子操作
Java可以通過(guò)鎖和CAS的方式實(shí)現(xiàn)原子操作。
循環(huán)CAS實(shí)現(xiàn)原子操作
JVM中的CAS操作正是利用處理器提供的CMPXCHG指令實(shí)現(xiàn)的。自旋CAS實(shí)現(xiàn)的基本思路就是循環(huán)進(jìn)行CAS操作直到成功為止。從Java1.5開始,JDK并發(fā)包里提供了一些類來(lái)支持原子操作,如:AtomicBoolean,AtomicInteger等。
CAS實(shí)現(xiàn)原子性的三大問(wèn)題
- ABA問(wèn)題。使用
AtomicStampReference類來(lái)解決。其會(huì)檢查當(dāng)前引用是否等于預(yù)期引用,當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志。簡(jiǎn)單理解就是加上時(shí)間戳或者加入一個(gè)標(biāo)志變量(線程修改一次就加一),讓舊值無(wú)法復(fù)原。 - 循環(huán)時(shí)間開銷大。這個(gè)暫時(shí)無(wú)法解決,《Java并發(fā)編程的藝術(shù)》一書給出的建議是JVM加入處理器提供的pause指令
- 只能保證一個(gè)共享變量的原子操作。解決方法:1)將多個(gè)共享變量合并成有一個(gè)共享變量來(lái)操作;2)使用
AtomicRederence類來(lái)保證引用對(duì)象之間的原子性,就可以把多個(gè)變量放在一個(gè)對(duì)象里進(jìn)行CAS操作。
總結(jié)
以上是生活随笔為你收集整理的java虚拟机线程调优与底层原理分析_Java并发编程——多线程的底层原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 手机换个麦克风多少钱?
- 下一篇: 我扑在书上下一句是什么啊?