深入了解Java中Synchronized關(guān)鍵字的實(shí)現(xiàn)原理
synchronized底層實(shí)現(xiàn)原理
synchronized 是 JVM 的內(nèi)置鎖,基于 Monitor 機(jī)制實(shí)現(xiàn)。每一個(gè)對象都有一個(gè)與之關(guān)聯(lián)的監(jiān)視器 (Monitor),這個(gè)監(jiān)視器充當(dāng)了一種互斥鎖的角色。當(dāng)一個(gè)線程想要訪問某個(gè)對象的 synchronized 代碼塊,首先需要獲取該對象的 Monitor。如果該 Monitor 已經(jīng)被其他線程持有,則當(dāng)前線程將會(huì)被阻塞,直至 Monitor 變?yōu)榭捎脿顟B(tài)。當(dāng)線程完成 synchronized 塊的代碼執(zhí)行后,它會(huì)釋放 Monitor,并把 Monitor 返還給對象池,這樣其他線程才能獲取 Monitor 并進(jìn)入 synchronized 代碼塊?,F(xiàn)在,讓我們一起深入理解 Monitor 是什么,以及它的工作機(jī)制。
什么是Monitor
在并發(fā)編程中,監(jiān)視器(Monitor)是Java虛擬機(jī)(JVM)的內(nèi)置同步機(jī)制,旨在實(shí)現(xiàn)線程之間的互斥訪問和協(xié)調(diào)。每個(gè)Java對象都有一個(gè)與之關(guān)聯(lián)的Monitor。這個(gè)Monitor的實(shí)現(xiàn)是在JVM的內(nèi)部完成的,它采用了一些底層的同步原語,用以實(shí)現(xiàn)線程間的等待和喚醒機(jī)制,這也是為什么等待(wait)和通知(notify)方法是屬于Object類的原因。這兩個(gè)方法實(shí)際上是通過操縱與對象關(guān)聯(lián)的Monitor,以完成線程的等待和喚醒操作,從而實(shí)現(xiàn)線程之間的同步。
在實(shí)現(xiàn)線程同步時(shí),Monitor 確實(shí)利用了 JVM 的內(nèi)存交互操作,包括 lock(鎖定)和 unlock(解鎖)指令。當(dāng)一個(gè)線程試圖獲取某個(gè)對象的 Monitor 鎖時(shí),它會(huì)執(zhí)行 lock 指令來嘗試獲取該鎖。如果這個(gè)鎖已經(jīng)被其他線程占有,那么當(dāng)前線程將會(huì)被阻塞,直至鎖變得可用。當(dāng)一個(gè)線程持有了 Monitor 鎖并且已完成對臨界區(qū)資源的操作后,它將會(huì)執(zhí)行 unlock 指令來釋放該鎖,從而使得其他線程有機(jī)會(huì)獲取該鎖并執(zhí)行相應(yīng)的臨界區(qū)代碼。如下圖一所示,
圖一
在jdk1.6后引入了偏向鎖,意思就是如果該同步塊沒有被其他線程占用,JVM會(huì)將對象頭中的標(biāo)記字段設(shè)置為偏向鎖,并將線程ID記錄在對象頭中,這個(gè)過程是通過CAS。值得注意的是,當(dāng)升級到重量級鎖時(shí),才會(huì)引入Monitor的概念。
Monitor在Java虛擬機(jī)中使用了MESA精簡模型來實(shí)現(xiàn)線程的等待和喚醒操作。那什么是MESA模型。
MESA模型
MESA模型是一種用于實(shí)現(xiàn)線程同步的模型,它提供了一種機(jī)制來實(shí)現(xiàn)線程之間的協(xié)作和通信。MESA模型提供了兩個(gè)基本操作:wait和signal(在Java中對應(yīng)為wait和notify/notifyAll),如圖二所示。
圖二
和我們Java中用到的不一樣,java中鎖的變量只有一個(gè)。
Monitor機(jī)制在Java中的實(shí)現(xiàn)
通過上邊了解到,Monitor機(jī)制提供了wait和notify,notiryAll方法,他們之間協(xié)作如下圖。
圖三
圖解釋:
- cxq (Contention Queue):是一個(gè)
棧結(jié)構(gòu)
先進(jìn)后出隊(duì)列。當(dāng)一個(gè)線程嘗試獲取已經(jīng)被其他線程占用的Monitor時(shí),如果嘗試失敗,這個(gè)線程會(huì)被加入到cxq中。 - EntryList:當(dāng)鎖釋放,會(huì)從這個(gè)隊(duì)列選出來第一個(gè)線程并執(zhí)行cas操作嘗試獲取鎖。
- WaitSet:FIFO(先進(jìn)先出)。當(dāng)一個(gè)線程調(diào)用了對象的wait()方法后,會(huì)被加入到這個(gè)隊(duì)列中。
cxq,EntryList和WaitSet他們之間是怎么協(xié)作的?
- 線程通過cas爭搶鎖,cas爭搶鎖失敗會(huì)進(jìn)入到cxq隊(duì)列中,
放到cxq的頭部
。cas成功就會(huì)獲得鎖。 - 當(dāng)鎖釋放會(huì)先從entryList中獲取第一個(gè)線程讓它c(diǎn)as操作,如果cas成功就獲得鎖。cas失敗(也就是說entryList第一個(gè)線程cas時(shí)候,恰好有另外一個(gè)線程執(zhí)行了cas并且成功了)。
- 如果entryList中沒有,會(huì)將cxq的全部線程一次性的放到entryList中,然后重新執(zhí)行上一步操作。
- 線程調(diào)用了wait操作,會(huì)將線程放到waitSet隊(duì)列的尾部。
- 當(dāng)其他線程執(zhí)行了notifyAll時(shí)候,會(huì)重新執(zhí)行第一步,也就是把所有的線程取出來然后開始cas操作嘗試獲取鎖,獲取失敗的就放到cxq中。
下邊用一個(gè)簡單的例子去說明:
有A,B,C,D四個(gè)線程。
- A線程執(zhí)行槍鎖操作成功獲得鎖。
- B線程執(zhí)行,C線程執(zhí)行,B,C都槍鎖失敗進(jìn)入cxq隊(duì)列。cxq隊(duì)列[C,B],EntryList和waitSet隊(duì)列為空。
- A執(zhí)行wait操作,釋放鎖,并進(jìn)入到waitSet隊(duì)列。cxq[C,B],EntryList[],WaisSet[A]。
- A釋放鎖后,先判斷EntryList隊(duì)列是否為空,如果為空,會(huì)將cxq隊(duì)列平移到EntryList隊(duì)列。cxq[],EntryList[C,B],waitSet[A]。
- 從EntryList頭部獲取一個(gè)線程進(jìn)行cas操作,C線程槍鎖成功,C現(xiàn)在獲得鎖?,F(xiàn)在cxq[],EntryList[B],waitSet[A]。
- 線程C執(zhí)行notifyAll操作,會(huì)把waitSet所有的等待線程取出來挨個(gè)cas嘗試獲取鎖,失敗的放入到cxq。cxq[A],EntryList[B],waitSet[]。
- D開始執(zhí)行,槍鎖失敗,進(jìn)入到cxq隊(duì)列。cxq[D,A],EntryList[B],waitSet[A]。
- 線程C執(zhí)行完畢,釋放鎖,從EntryList獲取一個(gè)線程并執(zhí)行,現(xiàn)在B出隊(duì)列獲取鎖,cxq[D,A],EntryList[],waitSet[]。。
- 當(dāng)B運(yùn)行完畢,由于EntryList為空,會(huì)從cxq中獲取并移動(dòng)到EntryList,執(zhí)行完之后列表編程cxq[],entryList[D,A],waitSet[]。
我們用代碼去演示。
代碼源碼:
public?static?void?main(String[]?args)?{ ??User?user?=?new?User(); ??Thread?A?=?new?Thread(()?->?{ ???synchronized?(user)?{ ????System.out.println(Thread.currentThread().getName()?+?"運(yùn)行"); ????ThreadUtil.sleep(3000L); ????System.out.println(Thread.currentThread().getName()?+?"調(diào)用wait"); ????try?{ ?????user.wait(); ????}?catch?(InterruptedException?e)?{ ?????e.printStackTrace(); ????} ????System.out.println(Thread.currentThread().getName()?+?"又繼續(xù)運(yùn)行"); ???} },?"線程A"); Thread?D?=?new?Thread(()?->?{ ??synchronized?(user)?{ ???System.out.println(Thread.currentThread().getName()?+?"運(yùn)行"); ??} ?},?"線程D"); ?Thread?B?=?new?Thread(()?->?{ ??synchronized?(user)?{ ????System.out.println(Thread.currentThread().getName()?+?"運(yùn)行"); ????user.notifyAll(); ????D.start(); ???} ??},?"線程B"); ??Thread?C?=?new?Thread(()?->?{ ???synchronized?(user)?{ ????System.out.println(Thread.currentThread().getName()?+?"運(yùn)行"); ????user.notifyAll(); ????D.start(); ???} ??},?"線程C"); ??A.start(); ??ThreadUtil.sleep(1000L); ??B.start(); ??C.start(); ?}
運(yùn)行結(jié)果如下:
線程A運(yùn)行 線程A調(diào)用wait 線程C運(yùn)行 線程B運(yùn)行 線程D運(yùn)行 線程A又繼續(xù)運(yùn)行
運(yùn)行流程
運(yùn)行流程
鎖的升級
在JDK 1.5之前,synchronized
關(guān)鍵字對應(yīng)的是重量級鎖,其涉及到操作系統(tǒng)對線程的調(diào)度,帶來較大的開銷。線程嘗試獲取一個(gè)已經(jīng)被其他線程持有的重量級鎖時(shí),它會(huì)進(jìn)入阻塞狀態(tài),直到鎖被釋放。這種阻塞涉及用戶態(tài)和核心態(tài)的切換,消耗大量資源。然而,實(shí)際上,線程持有鎖的時(shí)間大多數(shù)情況下是相當(dāng)短暫的,那么將線程掛起就顯得效率不高,存在優(yōu)化的空間。
JDK 1.6以后,Java引入了鎖的升級過程,即:無鎖-->偏向鎖-->輕量級鎖(自旋鎖)-->重量級鎖。這種優(yōu)化過程避免了一開始就采用重量級鎖,而是根據(jù)實(shí)際情況動(dòng)態(tài)地升級鎖的級別,能夠有效地降低資源消耗和提高并發(fā)性能。
「Java中對象的內(nèi)存布局:」
普通對象在內(nèi)存中分為三塊區(qū)域:對象頭、實(shí)例數(shù)據(jù)和對齊填充數(shù)據(jù)。對象頭包括Mark Word(8字節(jié))和類型指針(開啟壓縮指針時(shí)為4字節(jié),不開啟時(shí)為8字節(jié))。實(shí)例數(shù)據(jù)就是對象的成員變量。對齊填充數(shù)據(jù)用于保證對象的大小為8字節(jié)的倍數(shù),將對象所占字節(jié)數(shù)補(bǔ)到能被8整除。
內(nèi)存分布
經(jīng)典面試題,一個(gè)Object空對象占幾個(gè)字節(jié):
默認(rèn)開啟壓縮指針的情況下,64位機(jī)器:
Object o = new Object();(開啟指針壓縮)在內(nèi)存中占了 8(markWord)+4(classPointer)+4(padding)=16字節(jié)
64位對象頭mark work分布:
mark work分布
可以利用工具來查看鎖的內(nèi)存分布:
添加Maven
<groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> <scope>provided</scope>
使用方法:
在使用之前,設(shè)置JVM參數(shù),禁止延遲偏向,HotSpot 虛擬機(jī)在啟動(dòng)后有個(gè) 4s 的延遲才會(huì)對每個(gè)新建的對象開啟偏向鎖模式。
//禁止延遲偏向 -XX:BiasedLockingStartupDelay=0
public?static?void?main(String[]?args)?{ ?Object?o?=?new?Object(); ?synchronized?(o){ ??System.out.println(ClassLayout.parseInstance(o).toPrintable()); ?} }
內(nèi)存分布
前兩行顯示的是Mark Word,它占用8字節(jié)。第三行顯示的是類型指針(Class Pointer),它指向?qū)ο笏鶎兕惖脑獢?shù)據(jù)。由于JVM開啟了指針壓縮,所以類型指針占用4字節(jié)。第四行顯示的是對齊填充數(shù)據(jù),它用于保證對象大小為8字節(jié)的倍數(shù)。在這種情況下,由于對象頭占用12字節(jié),所以需要額外的4字節(jié)對齊填充數(shù)據(jù)來使整個(gè)對象占用16字節(jié)。
我們重點(diǎn)要看的就是我紅框標(biāo)記的那三位,那是鎖的狀態(tài)。
無鎖狀態(tài)
正如上圖所示那樣,001
表示無鎖狀態(tài)。沒有線程去獲得鎖。
偏向鎖
在沒有競爭
的情況下,偏向鎖會(huì)偏向于第一個(gè)訪問鎖的線程,讓這個(gè)線程以后每次訪問這個(gè)鎖時(shí)都不需要進(jìn)行同步。在第一次獲取偏向鎖的線程進(jìn)入同步塊時(shí),它會(huì)使用CAS操作嘗試將對象頭中的Mark Word更新為包含線程ID和偏向時(shí)間戳的值。
public?static?void?main(String[]?args)?{ ?Object?o?=?new?Object(); ?synchronized?(o){ ??System.out.println(ClassLayout.parseInstance(o).toPrintable()); ?} ?ThreadUtil.sleep(1000L); ?System.out.println(ClassLayout.parseInstance(o).toPrintable()); }
我們看下這個(gè)的偏向鎖的分布:
偏向鎖
圖分析,紅框標(biāo)注的101是偏向鎖,這時(shí)候發(fā)現(xiàn)持有鎖的時(shí)候和釋放鎖之后,兩個(gè)內(nèi)存分布是一樣的,這是因?yàn)?,在偏向鎖釋放后,對象的鎖標(biāo)志位仍然保持偏向鎖的狀態(tài),鎖記錄中的線程ID也不會(huì)被清空,偏向鎖的設(shè)計(jì)思想就是預(yù)計(jì)下一次還會(huì)有同一個(gè)線程再次獲得鎖,所以為了減少不必要的CAS操作(比較和交換),在沒有其他線程嘗試獲取鎖的情況下,會(huì)保持為偏向鎖狀態(tài),以提高性能。只有當(dāng)其他線程試圖獲取這個(gè)鎖時(shí),偏向鎖才會(huì)升級為輕量級鎖或者重量級鎖。
「那么下一個(gè)線程再來獲取偏向鎖,會(huì)發(fā)生什么?」
當(dāng)另一個(gè)線程嘗試獲取偏向鎖時(shí),會(huì)發(fā)生偏向鎖的撤銷,也稱為鎖撤銷。具體過程如下:
- 首先,需要檢查當(dāng)前持有偏向鎖的線程是否存活,這一步需要通過暫停該線程來完成。
- 如果持有偏向鎖的線程仍然存活,并持有該鎖,那么偏向鎖會(huì)被撤銷,并且升級為輕量級鎖。
- 如果持有偏向鎖的線程已經(jīng)不再存活,或者持有偏向鎖的線程并沒有在使用這個(gè)鎖,那么偏向鎖會(huì)被撤銷。在撤銷后,鎖會(huì)被設(shè)置為無鎖狀態(tài),此時(shí)其他線程可以嘗試獲取鎖。
- 如果在撤銷偏向鎖的過程中,有多個(gè)線程嘗試獲取鎖,那么鎖可能會(huì)直接升級為重量級鎖。
「偏向鎖調(diào)用hashCode會(huì)發(fā)生什么?」
在分析這個(gè)之前,需要先回顧上圖【mark word分布】。
當(dāng)一個(gè)對象調(diào)用原生的hashCode方法(來自O(shè)bject的,未被重寫過的)后,該對象將無法進(jìn)入偏向鎖狀態(tài),起步就會(huì)是輕量級鎖。如果hashCode方法的調(diào)用是在對象已經(jīng)處于偏向鎖狀態(tài)時(shí)調(diào)用,它的偏向狀態(tài)會(huì)被立即撤銷。在這種情況下,鎖會(huì)升級為重量級鎖。
這是因?yàn)槠蜴i在線程獲取偏向鎖時(shí),會(huì)用Thread ID和epoch值覆蓋identity hash code所在的位置。如果一個(gè)對象的hashCode方法已經(jīng)被調(diào)用過一次之后,這個(gè)對象還能被設(shè)置偏向鎖么?答案是不能。因?yàn)槿绻梢缘脑?,那Mark Word中的identity hash code必然會(huì)被偏向線程Id給覆蓋,這就會(huì)造成同一個(gè)對象前后兩次調(diào)用hashCode方法得到的結(jié)果不一致。
輕量級鎖會(huì)在鎖記錄中保存hashCode。
重量級鎖會(huì)在Monitor中記錄hashCode。
「偏向鎖調(diào)用wait/notify會(huì)發(fā)生什么?」
由上邊說到的synchronized底層實(shí)現(xiàn)原理知道,wait,notify,是Monitor提供的,像偏向鎖,輕量級鎖這些都是cas操作的不會(huì)用到Monitor,重量級鎖才會(huì)用到Monitor,所以當(dāng)調(diào)用wait/notify的時(shí)候就會(huì)升級到重量級鎖。
輕量級鎖
輕量級鎖主要用于線程交替執(zhí)行同步塊的場景,這種場景下,線程沒有真正的競爭,也就是有兩個(gè)線程,一個(gè)線程獲得了鎖,另一個(gè)線程在自旋,如果這時(shí)候第三個(gè)線程過來槍鎖,那就產(chǎn)生了真正的競爭了也就升級鎖。
「輕量級鎖的工作流程」
- 線程A獲取了鎖,并且鎖的狀態(tài)是偏向狀態(tài)。
- 線程B嘗試獲取鎖,發(fā)現(xiàn)鎖的Mark word中的線程id和自己的不一樣,并且線程還活著沒釋放鎖。
- 撤銷偏向鎖,暫停擁有偏向鎖的線程A,升級為輕量級鎖。
- 線程B會(huì)在自己的線程棧中創(chuàng)建Lock Record的空間,然后將鎖的Mark Word復(fù)制到LockRecord中。
- LockRecord里邊有一個(gè)字段叫做Owner,將Owner賦值成鎖地址。
- 線程B開始CAS操作,將鎖的mark Word轉(zhuǎn)換成Lock Record的地址。
- 如果失敗,鎖升級為重量級鎖。
- 如果成功,開始自旋操作,監(jiān)聽線程A是否釋放了鎖,默認(rèn)自旋十次。在 JDK1.6 之后,引入了自適應(yīng)自旋鎖,自適應(yīng)意味著自旋的次數(shù)不是固定不變的,而是根據(jù)前一次在同一個(gè)鎖上自旋的時(shí)間以及鎖的擁有者的狀態(tài)來決定。
重量級鎖
當(dāng)升級到到重量級鎖之后,意味著線程只能被掛起阻塞來等待被喚醒了,需要獲取到 Monitor 對象,線程被阻塞后便進(jìn)入內(nèi)核(Linux)調(diào)度狀態(tài),這個(gè)會(huì)導(dǎo)致系統(tǒng)在用戶態(tài)與內(nèi)核態(tài)之間來回切換,嚴(yán)重影響鎖的性能。
「鎖能降級嘛」
全局安全點(diǎn)是一個(gè)在所有線程都停止執(zhí)行常規(guī)代碼并執(zhí)行特定任務(wù)的點(diǎn),比如垃圾收集或線程棧調(diào)整。在全局安全點(diǎn),JVM有可能會(huì)嘗試降級鎖。
降級鎖的過程主要包括以下幾個(gè)步驟:
- 恢復(fù)鎖對象的MarkWord對象頭。這是因?yàn)樵谏墳橹亓考夋i的過程中,對象的MarkWord被改變了,所以在降級時(shí)需要恢復(fù)到原來的狀態(tài)。
- 重置ObjectMonitor對象。ObjectMonitor是用于管理鎖的一個(gè)對象,重置它的目的是為了準(zhǔn)備將鎖降級為輕量級鎖或偏向鎖。
- 將ObjectMonitor對象放入全局空閑列表。這是為了讓這個(gè)ObjectMonitor對象可以在后續(xù)被其他需要使用鎖的線程使用。
「為什么調(diào)用Object的wait/notify/notifyAll方法,需要加synchronized鎖?」
調(diào)用Object的wait/notify/notifyAll方法需要加synchronized鎖,是因?yàn)檫@些方法都會(huì)操作鎖對象。在synchronized底層,JVM使用了一個(gè)叫做Monitor的數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)鎖的功能。 當(dāng)一個(gè)線程調(diào)用wait方法時(shí),它會(huì)釋放鎖對象并進(jìn)入Monitor的WaitSet隊(duì)列等待。當(dāng)另一個(gè)線程調(diào)用notify或notifyAll方法時(shí),它會(huì)喚醒WaitSet隊(duì)列中的一個(gè)或多個(gè)線程,這些線程會(huì)重新競爭鎖對象。 由于wait/notify/notifyAll方法都會(huì)操作鎖對象,所以在調(diào)用這些方法之前,需要先獲取鎖對象。加synchronized鎖可以讓我們獲取到鎖對象。
以上就是深入了解Java中Synchronized關(guān)鍵字的實(shí)現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于Java Synchronized關(guān)鍵字的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot的切面應(yīng)用方式(注解Aspect)
文章總結(jié):Spring?Boot提供了三種攔截器:Filter、Interceptor和Aspect,Filter主要用于內(nèi)容過濾和非登錄狀態(tài)的非法請求過濾,無法獲取Spring框架相關(guān)的信息,Interceptor可以在獲取請求類名、方法名的同時(shí),獲取請求參數(shù),但無法獲取參數(shù)值2024-11-11Activiti7與Spring以及Spring Boot整合開發(fā)
這篇文章主要介紹了Activiti7與Spring以及Spring Boot整合開發(fā),在Activiti中核心類的是ProcessEngine流程引擎,與Spring整合就是讓Spring來管理ProcessEngine,有感興趣的同學(xué)可以參考閱讀2023-03-03JAVA基礎(chǔ)之控制臺(tái)輸入輸出的實(shí)例代碼
下面小編就為大家?guī)硪黄狫AVA基礎(chǔ)之控制臺(tái)輸入輸出的實(shí)例代碼。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-07-07重新啟動(dòng)IDEA時(shí)maven項(xiàng)目SSM框架文件變色所有@注解失效
這篇文章主要介紹了重新啟動(dòng)IDEA時(shí)maven項(xiàng)目SSM框架文件變色所有@注解失效,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03Java中break、continue、return在for循環(huán)中的使用
這篇文章主要介紹了break、continue、return在for循環(huán)中的使用,本文是小編收藏整理的,非常具有參考借鑒價(jià)值,需要的朋友可以參考下2017-11-11探討Java 將Markdown文件轉(zhuǎn)換為Word和PDF文檔
這篇文章主要介紹了Java 將Markdown文件轉(zhuǎn)換為Word和PDF文檔,本文通過分步指南及代碼示例展示了如何將 Markdown 文件轉(zhuǎn)換為 Word 文檔和 PDF 文件,需要的朋友可以參考下2024-07-07