欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

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

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

什么是Monitor

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

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

圖一

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

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

MESA模型

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

圖二

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

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

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

圖三

圖解釋:

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

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

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

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

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

  • A線程執(zhí)行槍鎖操作成功獲得鎖。
  • B線程執(zhí)行,C線程執(zhí)行,B,C都槍鎖失敗進入cxq隊列。cxq隊列[C,B],EntryList和waitSet隊列為空。
  • A執(zhí)行wait操作,釋放鎖,并進入到waitSet隊列。cxq[C,B],EntryList[],WaisSet[A]。
  • A釋放鎖后,先判斷EntryList隊列是否為空,如果為空,會將cxq隊列平移到EntryList隊列。cxq[],EntryList[C,B],waitSet[A]。
  • 從EntryList頭部獲取一個線程進行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í)行,槍鎖失敗,進入到cxq隊列。cxq[D,A],EntryList[B],waitSet[A]。
  • 線程C執(zhí)行完畢,釋放鎖,從EntryList獲取一個線程并執(zhí)行,現(xiàn)在B出隊列獲取鎖,cxq[D,A],EntryList[],waitSet[]。。
  • 當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()?+?"調用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();
?}

運行結果如下:

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

運行流程

運行流程

鎖的升級

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

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

「Java中對象的內存布局:」

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

內存分布

經典面試題,一個Object空對象占幾個字節(jié):

默認開啟壓縮指針的情況下,64位機器:

Object o = new Object();(開啟指針壓縮)在內存中占了 8(markWord)+4(classPointer)+4(padding)=16字節(jié)

64位對象頭mark work分布:

mark work分布

可以利用工具來查看鎖的內存分布:

添加Maven

<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
<scope>provided</scope>

使用方法:

在使用之前,設置JVM參數,禁止延遲偏向,HotSpot 虛擬機在啟動后有個 4s 的延遲才會對每個新建的對象開啟偏向鎖模式。

//禁止延遲偏向
-XX:BiasedLockingStartupDelay=0
public?static?void?main(String[]?args)?{
?Object?o?=?new?Object();
?synchronized?(o){
??System.out.println(ClassLayout.parseInstance(o).toPrintable());
?}
}

內存分布

前兩行顯示的是Mark Word,它占用8字節(jié)。第三行顯示的是類型指針(Class Pointer),它指向對象所屬類的元數據。由于JVM開啟了指針壓縮,所以類型指針占用4字節(jié)。第四行顯示的是對齊填充數據,它用于保證對象大小為8字節(jié)的倍數。在這種情況下,由于對象頭占用12字節(jié),所以需要額外的4字節(jié)對齊填充數據來使整個對象占用16字節(jié)。

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

無鎖狀態(tài)

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

偏向鎖

沒有競爭的情況下,偏向鎖會偏向于第一個訪問鎖的線程,讓這個線程以后每次訪問這個鎖時都不需要進行同步。在第一次獲取偏向鎖的線程進入同步塊時,它會使用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());
}

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

偏向鎖

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

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

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

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

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

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

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

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

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

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

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

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

輕量級鎖

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

「輕量級鎖的工作流程」

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

重量級鎖

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

「鎖能降級嘛」

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

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

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

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

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

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

相關文章

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

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

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

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

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

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

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

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

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

    JAVA基礎之控制臺輸入輸出的實例代碼

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

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

    這篇文章主要介紹了重新啟動IDEA時maven項目SSM框架文件變色所有@注解失效,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    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面向對象的封裝特征深度解析

    Java面向對象的封裝特征深度解析

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

    springboot整合freemarker的踩坑及解決

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

    探討Java 將Markdown文件轉換為Word和PDF文檔

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

最新評論