對于Java來說我們知道,Java代碼首先會編譯成Java字節(jié)碼工三,字節(jié)碼被類加載器加載到JVM里,JVM執(zhí)行字節(jié)碼先鱼,最終需要轉(zhuǎn)化為匯編指令在CPU上進(jìn)行執(zhí)行俭正。
Java中所使用的并發(fā)機(jī)制依賴于JVM的實現(xiàn)和CPU的指令。
下邊我們對常見的實現(xiàn)同步的兩個關(guān)鍵字volatile和synchronized進(jìn)行底層原理的分析焙畔,分析之余我們就會了解到JVM在對鎖的優(yōu)化所做的事情掸读,這樣的話我們以后在使用這兩個關(guān)鍵字的時候還可以游刃有余。
相關(guān)文章:
Java多線程編程-(2)-可重入鎖以及Synchronized的其他基本特性
在這一篇文章中儿惫,我們知道了volatile的兩個主要作用:一個是volatile可以禁止指令的重排序優(yōu)化,另一個作用是提供多線程訪問共享變量的內(nèi)存可見性伸但。?禁止指令重排序優(yōu)化是JVM內(nèi)存模型的知識點肾请,這里不做學(xué)習(xí),著重說一下可見性砌烁。
Java支持多個線程同時訪問一個對象或者對象的成員變量筐喳,由于每個線程可以擁有這個變量的拷貝(雖然對象以及成員變量分配的內(nèi)存是在共享內(nèi)存中的,但是每個執(zhí)行的線程還是可以擁有一份拷貝函喉,這樣做的目的是加速程序的執(zhí)行避归,這是現(xiàn)代多核處理器的一個顯著特性),所以程序在執(zhí)行過程中管呵,一個線程看到的變量并不一定是最新的梳毙。
關(guān)鍵字volatile可以用來修飾字段(成員變量),就是告知程序任何對該變量的訪問均需要從共享內(nèi)存中獲取捐下,而對它的改變必須同步刷新回共享內(nèi)存账锹,它能保證所有線程對變量訪問的可見性。
volatile是輕量級的synchronized坷襟,他的意思是:當(dāng)一個線程修改一個共享變量時奸柬,另外一個線程能讀到這個修改的值。如果volatile變量修飾符使用恰當(dāng)?shù)脑捰こ蹋萻ynchronized的使用和執(zhí)行成本更低廓奕,因為它不會引起線程上下文的切換和調(diào)度。
有volatile變量修飾的共享變量進(jìn)行寫操作的時候會引發(fā)了兩件事情:
(1)將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。
(2)這個寫回內(nèi)存的操作會使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效桌粉。
這是因為蒸绩,Java支持多個線程同時訪問一個對象或者對象的成員變量,每個線程可以擁有這個變量的拷貝(雖然對象以及成員變量分配的內(nèi)存是在共享內(nèi)存中的铃肯,但是每個執(zhí)行的線程還是可以擁有一份拷貝患亿,這樣做的目的是加速程序的執(zhí)行,這是現(xiàn)代多核處理器的一個顯著特性)押逼,所以程序在執(zhí)行過程中步藕,一個線程看到的變量并不一定是最新的。
我們的處理器為了提高處理速度挑格,處理器不直接和內(nèi)存進(jìn)行通信漱抓,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作恕齐,但操作完不知道何時會寫到內(nèi)存。
關(guān)鍵字volatile可以用來修飾字段瞬逊,就是告知程序任何對該變量的訪問均需要從共享內(nèi)存獲认云纭(讀取時將本地內(nèi)存置為無效,從共享內(nèi)存讀热纺鳌)士骤,而對它的改變必須同步刷新回共享內(nèi)存。保證所有線程對變量訪問的可見性蕾域。
具體的實現(xiàn)細(xì)節(jié)如下(不需深究其原理拷肌,把握住上述兩點即可):
如果對聲明了volatile的變量進(jìn)行寫操作,JVM就會向處理器發(fā)送一條Lock前綴的指令旨巷,將這個變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存巨缘。但是,就算寫回到內(nèi)存采呐,如果其他處理器緩存的值還是舊的若锁,再執(zhí)行計算操作就會有問題。所以斧吐,在多處理器下又固,為了保證各個處理器的緩存是一致的,就會實現(xiàn)緩存一致性協(xié)議煤率,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了仰冠,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改,就會將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài)蝶糯,當(dāng)處理器對這個數(shù)據(jù)進(jìn)行修改操作的時候洋只,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。
當(dāng)然,volatile的實現(xiàn)原理遠(yuǎn)不止這些木张,作為入門的理解我們牢牢把握住這兩點就行:一是強(qiáng)制把修改的數(shù)據(jù)寫回內(nèi)存众辨,另一個是在多處理器情況下使多處理器緩存的數(shù)據(jù)失效。這兩點對于一般的面試已經(jīng)足以稱下場面舷礼,如果還相對其有跟深入的理解鹃彻,可以另行搜索資源進(jìn)行學(xué)習(xí)。
關(guān)鍵字synchronized可以修飾方法或者以同步塊的形式來進(jìn)行使用蛛株,它主要確保多個線程在同一個時刻,只能有一個線程處于方法或者同步塊中育拨,它保證了線程對變量訪問的可見性和排他性谨履。
示例代碼:
public class Synchronized {
? ? public static void main(String[] args) {
? ? ? ? synchronized (Synchronized.class) {
? ? ? ? }
? ? ? ? method();
? ? }
? ? public static synchronized void method() {
? ? }
}
1
2
3
4
5
6
7
8
9
10
11
編譯運行,然后使用命令:javap.exe -v Synchronized.class熬丧,(javap在“Java\jdk1.8.0_131\bin”目錄下)查看結(jié)果:
大致可以看出笋粟,對于上述代碼中的同步塊?的實現(xiàn)是通過monitorenter和monitorexit?指令,而同步方法是依靠方法修飾符上的ACC_SYNCHRONIZED?來完成的析蝴。
上述的兩種方式害捕,無論采用的是哪一種方式,其本質(zhì)是對一個對象的監(jiān)視器(monitor)?進(jìn)行獲取闷畸,而這個獲取過程是排他的尝盼,也就是說同一時刻只有一個線程獲取到由synchronized所保護(hù)對象的監(jiān)視器。
synchronized允許使用任何的一個對象作為同步的內(nèi)容佑菩,因此任意一個對象都應(yīng)該擁有自己的監(jiān)視器(monitor)盾沫,當(dāng)這個對象由同步塊或者這個對象的同步方法調(diào)用時,執(zhí)行方法的線程必須先獲取到該對象的監(jiān)視器才能進(jìn)入同步塊或者同步方法殿漠,而沒有獲取到監(jiān)視器(執(zhí)行該方法)的線程將會被阻塞在同步塊和同步方法的入口處赴精,進(jìn)入BLOCKED狀態(tài)。
下圖描述了對象绞幌、對象的監(jiān)視器祖娘、同步隊列和執(zhí)行線程之間的關(guān)系:
從上圖中我們可以看到,任意線程對Object(Object由synchronized保護(hù))的訪問啊奄,首先要獲得Object的監(jiān)視器渐苏。如果獲取失敗,線程進(jìn)入同步隊列菇夸,線程狀態(tài)變?yōu)锽LOCKED琼富。當(dāng)訪問Object的前驅(qū)(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程庄新,使其重新嘗試對監(jiān)視器的獲取鞠眉。
三薯鼠、Java虛擬機(jī)對synchronized的優(yōu)化
synchronized相對于volatile是重量了很多,因此在以前很讓人詬病械蹋,但是從JDK 1.6版本以后為了減少獲得鎖和釋放鎖帶來的性能消耗而引入了偏向鎖和輕量級鎖出皇,以及鎖的存儲結(jié)構(gòu)和升級過程。
從JDK對synchronized的優(yōu)化哗戈,可以看出Java虛擬機(jī)對鎖優(yōu)化所做出的努力郊艘,下邊我們就分別學(xué)習(xí)一下什么是偏向鎖、輕量級鎖唯咬、重量級鎖纱注、自旋鎖。
在理解這四種鎖之前胆胰,我們先看一下synchronized鎖的存放位置狞贱,synchronized用的鎖是存在Java對象頭里的?,如果對象是數(shù)組類型蜀涨,則虛擬機(jī)用3個字寬(Word)存儲對象頭瞎嬉,如果對象是非數(shù)組類型,則用2字寬存儲對象頭厚柳。在32位虛擬機(jī)中佑颇,1字寬等于4字節(jié),即32bit草娜,如下圖:
Java對象頭里的Mark Word里默認(rèn)存儲對象的HashCode、分代年齡和鎖標(biāo)記位痒筒。32位JVM的Mark Word的默認(rèn)存儲結(jié)構(gòu)如下圖所示:
在Java SE 1.6中宰闰,鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)簿透、偏向鎖狀態(tài)移袍、輕量級鎖狀態(tài)和重量級鎖狀態(tài),這幾個狀態(tài)會隨著競爭情況逐漸升級老充。鎖可以升級但不能降級葡盗,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略啡浊,目的是為了提高獲得鎖和釋放鎖的效率觅够。
下邊分別研究一下這幾個狀態(tài)。
1喘先、偏向鎖核心思想
偏向鎖是一種針對加鎖操作的優(yōu)化手段,他的核心思想是:如果一個線程獲得了鎖廷粒,那么鎖就進(jìn)入了偏向模式窘拯。當(dāng)這個線程再次請求鎖時红且,無需再做任何同步操作,這樣就節(jié)省了大量有關(guān)鎖申請的操作涤姊,從而提高了程序的性能暇番。
2、偏向鎖設(shè)計初衷
為什么會出現(xiàn)這種設(shè)計的方式那思喊?這是因為根據(jù)HotSpot的作者研究壁酬,他發(fā)現(xiàn)鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得搔涝,為了讓線程獲得鎖的代價更低而引入了的偏向鎖這個概念厨喂。
3、偏向鎖獲取鎖流程
偏向鎖獲取鎖流程如下:
(1)當(dāng)一個線程訪問同步塊并獲取鎖時庄呈,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID蜕煌,以后該線程在進(jìn)入和退出同步塊時不需要進(jìn)行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖诬留;
(2)如果測試成功斜纪,表示線程已經(jīng)獲得了鎖。如果測試失敗文兑,則需要再測試一下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖)盒刚;
(3)如果沒有設(shè)置,則使用CAS競爭鎖绿贞;
(4)如果設(shè)置了因块,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程。
具體流程圖如下:
3籍铁、偏向鎖升級為輕量鎖
對于只有一個線程訪問的同步資源場景涡上,鎖的競爭不是很激烈,這時候使用偏向鎖是一種很好的選擇拒名,因為連續(xù)多次極有可能是同一個線程請求相同的鎖吩愧。
但是在鎖競爭比較激烈的場景,最有可能的情況是每次不同的線程來請求相同的鎖增显,這樣的話偏向鎖就會失效雁佳,倒不如不開啟這種模式,幸運的是Java虛擬機(jī)提供了參數(shù)可以讓我們有選擇的設(shè)置是否開啟偏向鎖同云。
如果偏向鎖失敗糖权,虛擬機(jī)并不會立即掛起線程,而是使用輕量級鎖進(jìn)行操作炸站。
如果偏向鎖失敗,虛擬機(jī)并不會立即掛起線程武契,而是使用輕量級鎖進(jìn)行操作募判。輕量級鎖他只是簡單的將對象頭部作為指針荡含,指向持有鎖的線程堆棧的內(nèi)部,來判斷一個線程是否持有對象鎖届垫。如果線程獲得輕量級鎖成功释液,則可以順利進(jìn)入臨界區(qū)。如果輕量級鎖加鎖失敗装处,則表示其他線程搶先奪到鎖误债,那么當(dāng)前線程的輕量級鎖就會膨脹為重量級鎖。
輕量級鎖就會膨脹為重量級鎖后寝蹈,虛擬機(jī)為了避免線程真實的在操作系統(tǒng)層面掛起,虛擬機(jī)還會在做最后的努力–自旋鎖登淘。
由于當(dāng)前線程暫時無法獲得鎖箫老,但是什么時候可以獲得鎖時一個未知數(shù)。也許在幾個CPU時鐘周期之后黔州,就可以獲得鎖耍鬓。如果是這樣的話,直接把線程掛起肯定是一種得不償失的選擇流妻,因此系統(tǒng)會在進(jìn)行一次努力:他會假設(shè)在不就的將來牲蜀,限額和從那個可以得到這把鎖,因此虛擬機(jī)會讓當(dāng)前線程做幾個空循環(huán)(這也就是自旋鎖的意義)绅这,若經(jīng)過幾個空循環(huán)可以獲取到鎖則進(jìn)入臨界區(qū)涣达,如果還是獲取不到則系統(tǒng)會真正的掛起線程。
那么為什么鎖的升級無法逆向那证薇?
這是因為度苔,自旋鎖無法預(yù)知到底會空循環(huán)幾個時鐘周期,并且會很消耗CPU棕叫,為了避免這種無用的自旋操作,一旦鎖升級為重量鎖奕删,就不會再恢復(fù)到輕量級鎖俺泣,這也是為什么一旦升級無法降級的原因所在。
從Java虛擬機(jī)在優(yōu)化synchronized的時候引入了:偏向鎖、輕量級鎖谨设、重量級鎖以及自旋鎖熟掂,都可以看出Java虛擬機(jī)通過各種方式,盡量減少獲取所和釋放鎖所帶來的性能消耗扎拣。
但這還不全是Java虛擬機(jī)鎖做的努力赴肚,另外還有:鎖消除?素跺、?CAS等等,更重要的還有一個無鎖的概念誉券,包括上文中提到的自旋鎖指厌,這些內(nèi)容會在后續(xù)的文章中繼續(xù)學(xué)習(xí)。
另外踊跟,關(guān)于本篇主題的講解踩验,可能偏向了volatile的原理講解、Java虛擬機(jī)對synchronized的優(yōu)化以及中間的幾種鎖商玫,這幾種鎖的具體含義也是面試的郴叮客,因此需要花時間靜下心來仔細(xì)研究一二拳昌。
如果想學(xué)習(xí)Java工程化袭异、高性能及分布式、深入淺出地回。性能調(diào)優(yōu)扁远、Spring,MyBatis刻像,Netty源碼分析的朋友可以加我的Java高級架構(gòu)進(jìn)階群:180705916畅买,群里有阿里大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費分享給大家