亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

深入了解Java中Synchronized關(guān)鍵字的實現(xiàn)原理

 更新時間:2023年06月12日 10:49:47   作者:西瓜西瓜大西瓜  
synchronized是JVM的內(nèi)置鎖,基于Monitor機(jī)制實現(xiàn),每一個對象都有一個與之關(guān)聯(lián)的監(jiān)視器?(Monitor),這個監(jiān)視器充當(dāng)了一種互斥鎖的角色,本文就詳細(xì)聊一聊Synchronized關(guān)鍵字的實現(xiàn)原理,需要的朋友可以參考下

synchronized底層實現(xiàn)原理

synchronized 是 JVM 的內(nèi)置鎖,基于 Monitor 機(jī)制實現(xiàn)。每一個對象都有一個與之關(guān)聯(lián)的監(jiān)視器 (Monitor),這個監(jiān)視器充當(dāng)了一種互斥鎖的角色。當(dāng)一個線程想要訪問某個對象的 synchronized 代碼塊,首先需要獲取該對象的 Monitor。如果該 Monitor 已經(jīng)被其他線程持有,則當(dāng)前線程將會被阻塞,直至 Monitor 變?yōu)榭捎脿顟B(tài)。當(dāng)線程完成 synchronized 塊的代碼執(zhí)行后,它會釋放 Monitor,并把 Monitor 返還給對象池,這樣其他線程才能獲取 Monitor 并進(jìn)入 synchronized 代碼塊?,F(xiàn)在,讓我們一起深入理解 Monitor 是什么,以及它的工作機(jī)制。

什么是Monitor

在并發(fā)編程中,監(jiān)視器(Monitor)是Java虛擬機(jī)(JVM)的內(nèi)置同步機(jī)制,旨在實現(xiàn)線程之間的互斥訪問和協(xié)調(diào)。每個Java對象都有一個與之關(guān)聯(lián)的Monitor。這個Monitor的實現(xiàn)是在JVM的內(nèi)部完成的,它采用了一些底層的同步原語,用以實現(xiàn)線程間的等待和喚醒機(jī)制,這也是為什么等待(wait)和通知(notify)方法是屬于Object類的原因。這兩個方法實際上是通過操縱與對象關(guān)聯(lián)的Monitor,以完成線程的等待和喚醒操作,從而實現(xiàn)線程之間的同步。

在實現(xiàn)線程同步時,Monitor 確實利用了 JVM 的內(nèi)存交互操作,包括 lock(鎖定)和 unlock(解鎖)指令。當(dāng)一個線程試圖獲取某個對象的 Monitor 鎖時,它會執(zhí)行 lock 指令來嘗試獲取該鎖。如果這個鎖已經(jīng)被其他線程占有,那么當(dāng)前線程將會被阻塞,直至鎖變得可用。當(dāng)一個線程持有了 Monitor 鎖并且已完成對臨界區(qū)資源的操作后,它將會執(zhí)行 unlock 指令來釋放該鎖,從而使得其他線程有機(jī)會獲取該鎖并執(zhí)行相應(yīng)的臨界區(qū)代碼。如下圖一所示,

圖一

在jdk1.6后引入了偏向鎖,意思就是如果該同步塊沒有被其他線程占用,JVM會將對象頭中的標(biāo)記字段設(shè)置為偏向鎖,并將線程ID記錄在對象頭中,這個過程是通過CAS。值得注意的是,當(dāng)升級到重量級鎖時,才會引入Monitor的概念。

Monitor在Java虛擬機(jī)中使用了MESA精簡模型來實現(xiàn)線程的等待和喚醒操作。那什么是MESA模型。

MESA模型

MESA模型是一種用于實現(xiàn)線程同步的模型,它提供了一種機(jī)制來實現(xiàn)線程之間的協(xié)作和通信。MESA模型提供了兩個基本操作:wait和signal(在Java中對應(yīng)為wait和notify/notifyAll),如圖二所示。

圖二

和我們Java中用到的不一樣,java中鎖的變量只有一個。

Monitor機(jī)制在Java中的實現(xiàn)

通過上邊了解到,Monitor機(jī)制提供了wait和notify,notiryAll方法,他們之間協(xié)作如下圖。

圖三

圖解釋:

  • cxq (Contention Queue):是一個棧結(jié)構(gòu)先進(jìn)后出隊列。當(dāng)一個線程嘗試獲取已經(jīng)被其他線程占用的Monitor時,如果嘗試失敗,這個線程會被加入到cxq中。
  • EntryList:當(dāng)鎖釋放,會從這個隊列選出來第一個線程并執(zhí)行cas操作嘗試獲取鎖。
  • WaitSet:FIFO(先進(jìn)先出)。當(dāng)一個線程調(diào)用了對象的wait()方法后,會被加入到這個隊列中。

cxq,EntryList和WaitSet他們之間是怎么協(xié)作的?

  • 線程通過cas爭搶鎖,cas爭搶鎖失敗會進(jìn)入到cxq隊列中,放到cxq的頭部。cas成功就會獲得鎖。
  • 當(dāng)鎖釋放會先從entryList中獲取第一個線程讓它cas操作,如果cas成功就獲得鎖。cas失敗(也就是說entryList第一個線程cas時候,恰好有另外一個線程執(zhí)行了cas并且成功了)。
  • 如果entryList中沒有,會將cxq的全部線程一次性的放到entryList中,然后重新執(zhí)行上一步操作。
  • 線程調(diào)用了wait操作,會將線程放到waitSet隊列的尾部。
  • 當(dāng)其他線程執(zhí)行了notifyAll時候,會重新執(zhí)行第一步,也就是把所有的線程取出來然后開始cas操作嘗試獲取鎖,獲取失敗的就放到cxq中。

下邊用一個簡單的例子去說明:

有A,B,C,D四個線程。

  • A線程執(zhí)行槍鎖操作成功獲得鎖。
  • B線程執(zhí)行,C線程執(zhí)行,B,C都槍鎖失敗進(jìn)入cxq隊列。cxq隊列[C,B],EntryList和waitSet隊列為空。
  • A執(zhí)行wait操作,釋放鎖,并進(jìn)入到waitSet隊列。cxq[C,B],EntryList[],WaisSet[A]。
  • A釋放鎖后,先判斷EntryList隊列是否為空,如果為空,會將cxq隊列平移到EntryList隊列。cxq[],EntryList[C,B],waitSet[A]。
  • 從EntryList頭部獲取一個線程進(jìn)行cas操作,C線程槍鎖成功,C現(xiàn)在獲得鎖?,F(xiàn)在cxq[],EntryList[B],waitSet[A]。
  • 線程C執(zhí)行notifyAll操作,會把waitSet所有的等待線程取出來挨個cas嘗試獲取鎖,失敗的放入到cxq。cxq[A],EntryList[B],waitSet[]。
  • D開始執(zhí)行,槍鎖失敗,進(jìn)入到cxq隊列。cxq[D,A],EntryList[B],waitSet[A]。
  • 線程C執(zhí)行完畢,釋放鎖,從EntryList獲取一個線程并執(zhí)行,現(xiàn)在B出隊列獲取鎖,cxq[D,A],EntryList[],waitSet[]。。
  • 當(dāng)B運行完畢,由于EntryList為空,會從cxq中獲取并移動到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()?+?"運行");
????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ù)運行");
???}
},?"線程A");
Thread?D?=?new?Thread(()?->?{
??synchronized?(user)?{
???System.out.println(Thread.currentThread().getName()?+?"運行");
??}
?},?"線程D");
?Thread?B?=?new?Thread(()?->?{
??synchronized?(user)?{
????System.out.println(Thread.currentThread().getName()?+?"運行");
????user.notifyAll();
????D.start();
???}
??},?"線程B");
??Thread?C?=?new?Thread(()?->?{
???synchronized?(user)?{
????System.out.println(Thread.currentThread().getName()?+?"運行");
????user.notifyAll();
????D.start();
???}
??},?"線程C");
??A.start();
??ThreadUtil.sleep(1000L);
??B.start();
??C.start();
?}

運行結(jié)果如下:

線程A運行 線程A調(diào)用wait 線程C運行 線程B運行 線程D運行 線程A又繼續(xù)運行

運行流程

運行流程

鎖的升級

在JDK 1.5之前,synchronized關(guān)鍵字對應(yīng)的是重量級鎖,其涉及到操作系統(tǒng)對線程的調(diào)度,帶來較大的開銷。線程嘗試獲取一個已經(jīng)被其他線程持有的重量級鎖時,它會進(jìn)入阻塞狀態(tài),直到鎖被釋放。這種阻塞涉及用戶態(tài)和核心態(tài)的切換,消耗大量資源。然而,實際上,線程持有鎖的時間大多數(shù)情況下是相當(dāng)短暫的,那么將線程掛起就顯得效率不高,存在優(yōu)化的空間。

JDK 1.6以后,Java引入了鎖的升級過程,即:無鎖-->偏向鎖-->輕量級鎖(自旋鎖)-->重量級鎖。這種優(yōu)化過程避免了一開始就采用重量級鎖,而是根據(jù)實際情況動態(tài)地升級鎖的級別,能夠有效地降低資源消耗和提高并發(fā)性能。

「Java中對象的內(nèi)存布局:」

普通對象在內(nèi)存中分為三塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充數(shù)據(jù)。對象頭包括Mark Word(8字節(jié))和類型指針(開啟壓縮指針時為4字節(jié),不開啟時為8字節(jié))。實例數(shù)據(jù)就是對象的成員變量。對齊填充數(shù)據(jù)用于保證對象的大小為8字節(jié)的倍數(shù),將對象所占字節(jié)數(shù)補到能被8整除。

內(nèi)存分布

經(jīng)典面試題,一個Object空對象占幾個字節(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ī)在啟動后有個 4s 的延遲才會對每個新建的對象開啟偏向鎖模式。

//禁止延遲偏向
-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ù)來使整個對象占用16字節(jié)。

我們重點要看的就是我紅框標(biāo)記的那三位,那是鎖的狀態(tài)。

無鎖狀態(tài)

正如上圖所示那樣,001表示無鎖狀態(tài)。沒有線程去獲得鎖。

偏向鎖

沒有競爭的情況下,偏向鎖會偏向于第一個訪問鎖的線程,讓這個線程以后每次訪問這個鎖時都不需要進(jìn)行同步。在第一次獲取偏向鎖的線程進(jìn)入同步塊時,它會使用CAS操作嘗試將對象頭中的Mark Word更新為包含線程ID和偏向時間戳的值。

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());
}

我們看下這個的偏向鎖的分布:

偏向鎖

圖分析,紅框標(biāo)注的101是偏向鎖,這時候發(fā)現(xiàn)持有鎖的時候和釋放鎖之后,兩個內(nèi)存分布是一樣的,這是因為,在偏向鎖釋放后,對象的鎖標(biāo)志位仍然保持偏向鎖的狀態(tài),鎖記錄中的線程ID也不會被清空,偏向鎖的設(shè)計思想就是預(yù)計下一次還會有同一個線程再次獲得鎖,所以為了減少不必要的CAS操作(比較和交換),在沒有其他線程嘗試獲取鎖的情況下,會保持為偏向鎖狀態(tài),以提高性能。只有當(dāng)其他線程試圖獲取這個鎖時,偏向鎖才會升級為輕量級鎖或者重量級鎖。

「那么下一個線程再來獲取偏向鎖,會發(fā)生什么?」

當(dāng)另一個線程嘗試獲取偏向鎖時,會發(fā)生偏向鎖的撤銷,也稱為鎖撤銷。具體過程如下:

  • 首先,需要檢查當(dāng)前持有偏向鎖的線程是否存活,這一步需要通過暫停該線程來完成。
  • 如果持有偏向鎖的線程仍然存活,并持有該鎖,那么偏向鎖會被撤銷,并且升級為輕量級鎖。
  • 如果持有偏向鎖的線程已經(jīng)不再存活,或者持有偏向鎖的線程并沒有在使用這個鎖,那么偏向鎖會被撤銷。在撤銷后,鎖會被設(shè)置為無鎖狀態(tài),此時其他線程可以嘗試獲取鎖。
  • 如果在撤銷偏向鎖的過程中,有多個線程嘗試獲取鎖,那么鎖可能會直接升級為重量級鎖。

「偏向鎖調(diào)用hashCode會發(fā)生什么?」

在分析這個之前,需要先回顧上圖【mark word分布】。

當(dāng)一個對象調(diào)用原生的hashCode方法(來自O(shè)bject的,未被重寫過的)后,該對象將無法進(jìn)入偏向鎖狀態(tài),起步就會是輕量級鎖。如果hashCode方法的調(diào)用是在對象已經(jīng)處于偏向鎖狀態(tài)時調(diào)用,它的偏向狀態(tài)會被立即撤銷。在這種情況下,鎖會升級為重量級鎖。

這是因為偏向鎖在線程獲取偏向鎖時,會用Thread ID和epoch值覆蓋identity hash code所在的位置。如果一個對象的hashCode方法已經(jīng)被調(diào)用過一次之后,這個對象還能被設(shè)置偏向鎖么?答案是不能。因為如果可以的話,那Mark Word中的identity hash code必然會被偏向線程Id給覆蓋,這就會造成同一個對象前后兩次調(diào)用hashCode方法得到的結(jié)果不一致。

輕量級鎖會在鎖記錄中保存hashCode。

重量級鎖會在Monitor中記錄hashCode。

「偏向鎖調(diào)用wait/notify會發(fā)生什么?」

由上邊說到的synchronized底層實現(xiàn)原理知道,wait,notify,是Monitor提供的,像偏向鎖,輕量級鎖這些都是cas操作的不會用到Monitor,重量級鎖才會用到Monitor,所以當(dāng)調(diào)用wait/notify的時候就會升級到重量級鎖。

輕量級鎖

輕量級鎖主要用于線程交替執(zhí)行同步塊的場景,這種場景下,線程沒有真正的競爭,也就是有兩個線程,一個線程獲得了鎖,另一個線程在自旋,如果這時候第三個線程過來槍鎖,那就產(chǎn)生了真正的競爭了也就升級鎖。

「輕量級鎖的工作流程」

  • 線程A獲取了鎖,并且鎖的狀態(tài)是偏向狀態(tài)。
  • 線程B嘗試獲取鎖,發(fā)現(xiàn)鎖的Mark word中的線程id和自己的不一樣,并且線程還活著沒釋放鎖。
  • 撤銷偏向鎖,暫停擁有偏向鎖的線程A,升級為輕量級鎖。
  • 線程B會在自己的線程棧中創(chuàng)建Lock Record的空間,然后將鎖的Mark Word復(fù)制到LockRecord中。
  • LockRecord里邊有一個字段叫做Owner,將Owner賦值成鎖地址。
  • 線程B開始CAS操作,將鎖的mark Word轉(zhuǎn)換成Lock Record的地址。
  • 如果失敗,鎖升級為重量級鎖。
  • 如果成功,開始自旋操作,監(jiān)聽線程A是否釋放了鎖,默認(rèn)自旋十次。在 JDK1.6 之后,引入了自適應(yīng)自旋鎖,自適應(yīng)意味著自旋的次數(shù)不是固定不變的,而是根據(jù)前一次在同一個鎖上自旋的時間以及鎖的擁有者的狀態(tài)來決定。

重量級鎖

當(dāng)升級到到重量級鎖之后,意味著線程只能被掛起阻塞來等待被喚醒了,需要獲取到 Monitor 對象,線程被阻塞后便進(jìn)入內(nèi)核(Linux)調(diào)度狀態(tài),這個會導(dǎo)致系統(tǒng)在用戶態(tài)與內(nèi)核態(tài)之間來回切換,嚴(yán)重影響鎖的性能。

「鎖能降級嘛」

全局安全點是一個在所有線程都停止執(zhí)行常規(guī)代碼并執(zhí)行特定任務(wù)的點,比如垃圾收集或線程棧調(diào)整。在全局安全點,JVM有可能會嘗試降級鎖。

降級鎖的過程主要包括以下幾個步驟:

  • 恢復(fù)鎖對象的MarkWord對象頭。這是因為在升級為重量級鎖的過程中,對象的MarkWord被改變了,所以在降級時需要恢復(fù)到原來的狀態(tài)。
  • 重置ObjectMonitor對象。ObjectMonitor是用于管理鎖的一個對象,重置它的目的是為了準(zhǔn)備將鎖降級為輕量級鎖或偏向鎖。
  • 將ObjectMonitor對象放入全局空閑列表。這是為了讓這個ObjectMonitor對象可以在后續(xù)被其他需要使用鎖的線程使用。

「為什么調(diào)用Object的wait/notify/notifyAll方法,需要加synchronized鎖?」

調(diào)用Object的wait/notify/notifyAll方法需要加synchronized鎖,是因為這些方法都會操作鎖對象。在synchronized底層,JVM使用了一個叫做Monitor的數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)鎖的功能。 當(dāng)一個線程調(diào)用wait方法時,它會釋放鎖對象并進(jìn)入Monitor的WaitSet隊列等待。當(dāng)另一個線程調(diào)用notify或notifyAll方法時,它會喚醒WaitSet隊列中的一個或多個線程,這些線程會重新競爭鎖對象。 由于wait/notify/notifyAll方法都會操作鎖對象,所以在調(diào)用這些方法之前,需要先獲取鎖對象。加synchronized鎖可以讓我們獲取到鎖對象。

以上就是深入了解Java中Synchronized關(guān)鍵字的實現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于Java Synchronized關(guān)鍵字的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • springboot的切面應(yīng)用方式(注解Aspect)

    springboot的切面應(yīng)用方式(注解Aspect)

    文章總結(jié):Spring?Boot提供了三種攔截器:Filter、Interceptor和Aspect,Filter主要用于內(nèi)容過濾和非登錄狀態(tài)的非法請求過濾,無法獲取Spring框架相關(guān)的信息,Interceptor可以在獲取請求類名、方法名的同時,獲取請求參數(shù),但無法獲取參數(shù)值
    2024-11-11
  • Activiti7與Spring以及Spring Boot整合開發(fā)

    Activiti7與Spring以及Spring Boot整合開發(fā)

    這篇文章主要介紹了Activiti7與Spring以及Spring Boot整合開發(fā),在Activiti中核心類的是ProcessEngine流程引擎,與Spring整合就是讓Spring來管理ProcessEngine,有感興趣的同學(xué)可以參考閱讀
    2023-03-03
  • 淺談一下Java中的悲觀鎖和樂觀鎖

    淺談一下Java中的悲觀鎖和樂觀鎖

    這篇文章主要介紹了一下Java中的悲觀鎖和樂觀鎖,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04
  • java中aop實現(xiàn)接口訪問頻率限制

    java中aop實現(xiàn)接口訪問頻率限制

    本文主要介紹了java中aop實現(xiàn)接口訪問頻率限制,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04
  • JAVA基礎(chǔ)之控制臺輸入輸出的實例代碼

    JAVA基礎(chǔ)之控制臺輸入輸出的實例代碼

    下面小編就為大家?guī)硪黄狫AVA基礎(chǔ)之控制臺輸入輸出的實例代碼。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-07-07
  • 重新啟動IDEA時maven項目SSM框架文件變色所有@注解失效

    重新啟動IDEA時maven項目SSM框架文件變色所有@注解失效

    這篇文章主要介紹了重新啟動IDEA時maven項目SSM框架文件變色所有@注解失效,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-03-03
  • Java中break、continue、return在for循環(huán)中的使用

    Java中break、continue、return在for循環(huán)中的使用

    這篇文章主要介紹了break、continue、return在for循環(huán)中的使用,本文是小編收藏整理的,非常具有參考借鑒價值,需要的朋友可以參考下
    2017-11-11
  • Java面向?qū)ο蟮姆庋b特征深度解析

    Java面向?qū)ο蟮姆庋b特征深度解析

    在面向?qū)ο蟪淌皆O(shè)計方法中,封裝(英語:Encapsulation)是指一種將抽象性函式接口的實現(xiàn)細(xì)節(jié)部分包裝、隱藏起來的方法。封裝可以被認(rèn)為是一個保護(hù)屏障,防止該類的代碼和數(shù)據(jù)被外部類定義的代碼隨機(jī)訪問
    2021-10-10
  • springboot整合freemarker的踩坑及解決

    springboot整合freemarker的踩坑及解決

    這篇文章主要介紹了springboot整合freemarker的踩坑及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-05-05
  • 探討Java 將Markdown文件轉(zhuǎn)換為Word和PDF文檔

    探討Java 將Markdown文件轉(zhuǎn)換為Word和PDF文檔

    這篇文章主要介紹了Java 將Markdown文件轉(zhuǎn)換為Word和PDF文檔,本文通過分步指南及代碼示例展示了如何將 Markdown 文件轉(zhuǎn)換為 Word 文檔和 PDF 文件,需要的朋友可以參考下
    2024-07-07

最新評論